blob: 4c131b1e46d68270e9cea973e863a4a509d2086e [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001/**
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
23var _stops = new Array(); // list of stop objects
24var _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.
27var _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 */
37var _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.
44var _stopListener = {
45 pre: [],
46 post: []
47};
48
49/**
50 * Remove all stop previously identified stop elements.
51 */
52function _reset() {
53 _stops = new Array();
54}
55
56function _get(i) {
57 return _stops[i];
58}
59
60function _set(i, element) {
61 _stops[i] = element;
62}
63
64function _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
74function _append(element) {
75 _stops.push(element);
76}
77
78function _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
88function _length() {
89 return _stops.length;
90}
91
92/**
93 * Sets the named option to the specified value.
94 */
95function _setOption(name, value) {
96 _options[name] = value;
97}
98
99/**
100 * Register a key to move forward one stop.
101 */
102function _addFwdKey(character) {
103 kibbles.keys.addKeyPressListener(character, _gotoNextStop);
104}
105
106/**
107 * Register a key to move back one stop.
108 */
109function _addRevKey(character) {
110 kibbles.keys.addKeyPressListener(character, _gotoPreviousStop);
111}
112
113/**
114 * Adds a stop listener.
115 */
116function _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 */
127function _gotoNextStop() {
128 _setCurrentStop(_getNextStop());
129}
130
131/**
132 * Scroll to previous stop if any.
133 */
134function _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 */
142function _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 */
168function _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
196function _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 */
212function _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 */
223function _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 */
238function _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 */
256function _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 */
274function 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 */
284function _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
300if (!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 */
313window.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.
333kibbles.keys.listen();
334
335})();