Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 1 | /** |
| 2 | * Copyright 2008 Steve McKay. |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | /** |
| 18 | * Kibbles.Skipper is a Javascript library providing support for keyboard |
| 19 | * navigation among DOM object on a page. |
| 20 | */ |
| 21 | (function(){ |
| 22 | |
| 23 | var _stops = new Array(); // list of stop objects |
| 24 | var _lastStop; // id of the last stop we visited to. |
| 25 | |
| 26 | // Named options. The value can be a literal value, or a function to call. |
| 27 | var _options = { |
| 28 | padding_top: 0, // window offset when scrolling |
| 29 | padding_bottom: 0, |
| 30 | scroll_window: true |
| 31 | }; |
| 32 | |
| 33 | /* |
| 34 | * Constants identifying listener types. Used with the method that |
| 35 | * enables registration of listeners. |
| 36 | */ |
| 37 | var _LISTENER_TYPE = { |
| 38 | PRE: 'pre', |
| 39 | POST: 'post' |
| 40 | }; |
| 41 | |
| 42 | // map of stop listeners by type. pre listeners are called before navigation |
| 43 | // post listeners are called after navigation. |
| 44 | var _stopListener = { |
| 45 | pre: [], |
| 46 | post: [] |
| 47 | }; |
| 48 | |
| 49 | /** |
| 50 | * Remove all stop previously identified stop elements. |
| 51 | */ |
| 52 | function _reset() { |
| 53 | _stops = new Array(); |
| 54 | } |
| 55 | |
| 56 | function _get(i) { |
| 57 | return _stops[i]; |
| 58 | } |
| 59 | |
| 60 | function _set(i, element) { |
| 61 | _stops[i] = element; |
| 62 | } |
| 63 | |
| 64 | function _insert(i, element) { |
| 65 | if (i < 0 || i > _stops.length - 1) { |
| 66 | throw "Index out of bounds."; |
| 67 | } |
| 68 | _stops.splice(i, 0, element); |
| 69 | if (i <= _lastStop) { |
| 70 | _lastStop++; |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | function _append(element) { |
| 75 | _stops.push(element); |
| 76 | } |
| 77 | |
| 78 | function _del(i) { |
| 79 | if (i < 0 || i > _stops.length - 1) { |
| 80 | throw "Index out of bounds."; |
| 81 | } |
| 82 | _stops.splice(i, 1); |
| 83 | if (_lastStop >= i) { |
| 84 | _lastStop--; |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | function _length() { |
| 89 | return _stops.length; |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * Sets the named option to the specified value. |
| 94 | */ |
| 95 | function _setOption(name, value) { |
| 96 | _options[name] = value; |
| 97 | } |
| 98 | |
| 99 | /** |
| 100 | * Register a key to move forward one stop. |
| 101 | */ |
| 102 | function _addFwdKey(character) { |
| 103 | kibbles.keys.addKeyPressListener(character, _gotoNextStop); |
| 104 | } |
| 105 | |
| 106 | /** |
| 107 | * Register a key to move back one stop. |
| 108 | */ |
| 109 | function _addRevKey(character) { |
| 110 | kibbles.keys.addKeyPressListener(character, _gotoPreviousStop); |
| 111 | } |
| 112 | |
| 113 | /** |
| 114 | * Adds a stop listener. |
| 115 | */ |
| 116 | function _addStopListener(type, handler) { |
| 117 | if (type == _LISTENER_TYPE.PRE) { |
| 118 | _stopListener.pre.push(handler); |
| 119 | } else if (type == _LISTENER_TYPE.POST) { |
| 120 | _stopListener.post.push(handler); |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * Scroll to next stop if any. |
| 126 | */ |
| 127 | function _gotoNextStop() { |
| 128 | _setCurrentStop(_getNextStop()); |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * Scroll to previous stop if any. |
| 133 | */ |
| 134 | function _gotoPreviousStop() { |
| 135 | _setCurrentStop(_getPreviousStop()); |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * Update the current and previous stops, scrolling window to the location |
| 140 | * of the specified stop, and notifying listeners in the process. |
| 141 | */ |
| 142 | function _setCurrentStop(i) { |
| 143 | if (i >= 0) { |
| 144 | var prevStop = _lastStop; |
| 145 | _lastStop = i; |
| 146 | |
| 147 | var next = new Stop(i); |
| 148 | var prev = (prevStop >= 0) ? new Stop(prevStop) : undefined; |
| 149 | |
| 150 | _notifyListeners(next, prev, _stopListener.pre); |
| 151 | |
| 152 | // If the y coord of the stop was not previously determined |
| 153 | // it may have been hidden. Since "PRE" listeners may reveal |
| 154 | // hidden stops, we try again if "y" is not know. |
| 155 | if (!next.y) next.y = _findObjectPosition(next.element); |
| 156 | |
| 157 | // if we can't id the y coords at this point, we throw an exception. |
| 158 | if (!next.y && !(next.y >= 0)) { |
| 159 | throw "Next stop does not y coords. Aborting."; |
| 160 | } |
| 161 | _notifyListeners(next, prev, _stopListener.post); |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | /** |
| 166 | * Called by a listener, not directly. |
| 167 | */ |
| 168 | function _scrollOpportunityListener(next, prev) { |
| 169 | if (!_getOptionValue('scroll_window')) return; |
| 170 | |
| 171 | if (next && next.element) { |
| 172 | |
| 173 | var viewTop = _windowScrollTop(); |
| 174 | var viewBottom = viewTop + document.documentElement.clientHeight; |
| 175 | |
| 176 | var padTop = _getOptionValue('padding_top'); |
| 177 | |
| 178 | var bottom = viewBottom - padTop; |
| 179 | |
| 180 | // if we skipped below the bottom padding |
| 181 | if (next.y > bottom) { |
| 182 | window.scrollTo(0, next.y - padTop); |
| 183 | return; |
| 184 | } |
| 185 | |
| 186 | var padBottom = _getOptionValue('padding_bottom'); |
| 187 | // if we skipped above the top offset |
| 188 | var top = viewTop + padBottom; |
| 189 | if (next.y < top) { |
| 190 | window.scrollTo(0, (next.y - document.documentElement.clientHeight) + padBottom); |
| 191 | return; |
| 192 | } |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | function _windowScrollTop() { |
| 197 | if (window.document.body.scrollTop) { |
| 198 | return window.document.body.scrollTop; |
| 199 | } else if (window.document.documentElement.scrollTop) { |
| 200 | return window.document.documentElement.scrollTop; |
| 201 | } else if (window.pageYOffset) { |
| 202 | return window.pageYOffset; |
| 203 | } |
| 204 | return 0; |
| 205 | } |
| 206 | |
| 207 | |
| 208 | /** |
| 209 | * Returns an option value or if the value is a function, |
| 210 | * the value returned by the function. |
| 211 | */ |
| 212 | function _getOptionValue(name) { |
| 213 | var opt = _options[name]; |
| 214 | if (typeof opt == "function") { |
| 215 | return opt(); |
| 216 | } |
| 217 | return opt; |
| 218 | } |
| 219 | |
| 220 | /** |
| 221 | * Notify all supplied stop listeners. |
| 222 | */ |
| 223 | function _notifyListeners(stop, previousStop, listeners) { |
| 224 | if (stop && listeners) { |
| 225 | try { |
| 226 | for (var i = 0; i < listeners.length; i++) { |
| 227 | listeners[i](stop, previousStop); |
| 228 | } |
| 229 | } catch(err) { |
| 230 | // don't let a grumpy listener bring us down. |
| 231 | } |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | /** |
| 236 | * Returns the next stop or null if none stop available. |
| 237 | */ |
| 238 | function _getNextStop() { |
| 239 | var i = 0; |
| 240 | |
| 241 | // if we've already visited a stop, use that as the base for the next stop. |
| 242 | if (_lastStop >= 0) { |
| 243 | i = _lastStop + 1; |
| 244 | } |
| 245 | |
| 246 | // if the presumed next stop is out of bounds, return null. |
| 247 | if (i > _stops.length - 1) { |
| 248 | return; |
| 249 | } |
| 250 | return i; |
| 251 | } |
| 252 | |
| 253 | /** |
| 254 | * Returns the previous stop or null if none available. |
| 255 | */ |
| 256 | function _getPreviousStop() { |
| 257 | var i = _stops.length - 1; |
| 258 | |
| 259 | // if we've already visited a stop, use that as the base for the next stop. |
| 260 | if (_lastStop >= 0) { |
| 261 | i = _lastStop - 1; |
| 262 | } |
| 263 | |
| 264 | // if the presumed next stop is out of bounds, return null. |
| 265 | if (i < 0) { |
| 266 | return; |
| 267 | } |
| 268 | return i; |
| 269 | } |
| 270 | |
| 271 | /** |
| 272 | * Convenience wrapper for "stop" related information. |
| 273 | */ |
| 274 | function Stop(i, y) { |
| 275 | this.index = i; |
| 276 | this.element = _stops[i]; |
| 277 | this.y = _findObjectPosition(this.element); |
| 278 | } |
| 279 | |
| 280 | /** |
| 281 | * Returns the vertical coordinate of the top of specified object |
| 282 | * relative to the top of the entire page. |
| 283 | */ |
| 284 | function _findObjectPosition(obj) { |
| 285 | if (obj) { |
| 286 | var curtop = 0; |
| 287 | if (obj.offsetParent) { |
| 288 | while (obj.offsetParent) { |
| 289 | curtop += obj.offsetTop; |
| 290 | obj = obj.offsetParent; |
| 291 | } |
| 292 | } else if (obj.y) { |
| 293 | curtop += obj.y; |
| 294 | } |
| 295 | return curtop; |
| 296 | } |
| 297 | return null; |
| 298 | } |
| 299 | |
| 300 | if (!window.kibbles.keys) { |
| 301 | throw "Kibbles.Skipper requires Kibbles.Keys which is not loaded." |
| 302 | + " Can't continue."; |
| 303 | } |
| 304 | |
| 305 | /** |
| 306 | * A nice little namespace to call our own. |
| 307 | * |
| 308 | * Formalizing Kibbles.Skipper as a traditional javascript class caused |
| 309 | * headaches with respect to capturing the context (what is "this" |
| 310 | * at any point in time). So we use a simple script exported via the |
| 311 | * "kibbles.skipper" namespace. |
| 312 | */ |
| 313 | window.kibbles.skipper = { |
| 314 | setOption: _setOption, |
| 315 | addFwdKey: _addFwdKey, |
| 316 | addRevKey: _addRevKey, |
| 317 | LISTENER_TYPE: _LISTENER_TYPE, |
| 318 | addStopListener: _addStopListener, |
| 319 | setCurrentStop: _setCurrentStop, |
| 320 | // array like methods for stop manipulation |
| 321 | get: _get, |
| 322 | set: _set, |
| 323 | append: _append, |
| 324 | insert: _insert, |
| 325 | del: _del, |
| 326 | length: _length, |
| 327 | reset: _reset |
| 328 | } |
| 329 | |
| 330 | _addStopListener(kibbles.skipper.LISTENER_TYPE.POST, _scrollOpportunityListener) |
| 331 | |
| 332 | // we depend on kibbles.keys. |
| 333 | kibbles.keys.listen(); |
| 334 | |
| 335 | })(); |