├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── cjs └── index.js ├── esm └── index.js ├── index.js ├── package.json └── test ├── css └── index.css ├── favicon.ico ├── img ├── tweety_112.png └── tweety_56.png ├── index.html ├── index.js ├── js ├── index.js └── navikeytor.js ├── manifest.webapp └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | test/node_modules/* 2 | node_modules/* 3 | package-lock.json 4 | test/package-lock.json 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/index.js 2 | test/ 3 | .gitignore 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2018, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # navikeytor 2 | 100% keyboard based spatial navigation. 3 | 4 | **Work In Progress** 5 | 6 | It targets any element with class `key` and it groups horizontally every `key` within a container with class `keys`. 7 | 8 | It fully preserves tab navigation, and its major target are feature phones. 9 | 10 | ```js 11 | // target the whole document 12 | new Navikeytor(document) 13 | // each event is registered as `navikeytor:${type}` 14 | .on('back', event => { 15 | // details has {event, target} 16 | // as original event, and target node 17 | // which is the currently focused/active one 18 | console.log(event.type, event.detail); 19 | }) 20 | .on('bar', event => { 21 | console.log(event.type, event.detail); 22 | }) 23 | .on('click', event => { 24 | console.log(event.type, event.detail); 25 | }) 26 | .on('dblbar', event => { 27 | console.log(event.type, event.detail); 28 | }) 29 | .on('dblclick', event => { 30 | console.log(event.type, event.detail); 31 | }) 32 | .on('esc', event => { 33 | console.log(event.type, event.detail); 34 | }) 35 | .on('next', event => { 36 | console.log(event.type, event.detail); 37 | }) 38 | .on('prev', event => { 39 | console.log(event.type, event.detail); 40 | }) 41 | ; 42 | ``` 43 | 44 | ### Live Demo 45 | 46 | You can try both [ltr](https://webreflection.github.io/navikeytor/test/) use case or [rtl](https://webreflection.github.io/navikeytor/test/?rtl). 47 | 48 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | /*! (c) 2018 Andrea Giammarchi (ISC) */ 2 | 3 | Navikeytor.prefix = 'navikeytor:'; 4 | Navikeytor.delay = 250; 5 | function Navikeytor(el) {"use strict"; 6 | this._lastTarget = null; 7 | this.el = el; 8 | this.barTimer = this.clickTimer = 0; 9 | this.document = el.ownerDocument || el; 10 | this.key = el.getElementsByClassName('key'); 11 | this.start(); 12 | } 13 | 14 | try { 15 | new CustomEvent(Navikeytor.prefix); 16 | Navikeytor.CE = CustomEvent; 17 | } catch(IE) { 18 | Navikeytor.CE = function (type, options) { 19 | var event = document.createEvent('CustomEvent'); 20 | event.initCustomEvent( 21 | type, 22 | !!options.bubbles, 23 | !!options.cancelable, 24 | options.detail 25 | ); 26 | return event; 27 | }; 28 | } 29 | 30 | Navikeytor.prototype = { 31 | constructor: Navikeytor, 32 | dispatch: function (type, detail) { 33 | return this.el.dispatchEvent(new Navikeytor.CE( 34 | Navikeytor.prefix + type, 35 | { 36 | detail: detail, 37 | bubbles: true 38 | } 39 | )); 40 | }, 41 | handleEvent: function (event) { 42 | this['_on' + event.type](event); 43 | }, 44 | off: function (type, listener) { 45 | this.el.removeEventListener(Navikeytor.prefix + type, listener, false); 46 | return this; 47 | }, 48 | on: function (type, listener) { 49 | this.el.addEventListener(Navikeytor.prefix + type, listener, false); 50 | return this; 51 | }, 52 | start: function () { 53 | this.el.addEventListener('keydown', this, false); 54 | if ('ontouchend' in this.el) 55 | this._touch(1); 56 | }, 57 | stop: function () { 58 | this.el.removeEventListener('keydown', this, false); 59 | clearTimeout(this.barTimer); 60 | clearTimeout(this.clickTimer); 61 | this.barTimer = this.clickTimer = 0; 62 | if ('ontouchend' in this.el) 63 | this._touch(0); 64 | }, 65 | _indexOf: [].indexOf, 66 | _active: function () { 67 | var body = this.document.body; 68 | var activeElement = this.document.activeElement || body; 69 | if (activeElement === body && this._lastTarget !== null) 70 | return this._lastTarget; 71 | return activeElement; 72 | }, 73 | _bar: function (self, event) { 74 | self.barTimer = 0; 75 | self.dispatch('bar', { 76 | event: event, 77 | target: self._active() 78 | }); 79 | }, 80 | _click: function (self, event) { 81 | self.clickTimer = 0; 82 | self.dispatch('click', { 83 | event: event, 84 | target: self._active() 85 | }); 86 | }, 87 | _closest: function (el, css) { 88 | return (el.closest || this._closestFix).call(el, css); 89 | }, 90 | _closestFix: function (css) { 91 | var parentNode = this, matches; 92 | while ( 93 | (matches = parentNode && parentNode.matches) && 94 | !parentNode.matches(css) 95 | ) 96 | parentNode = parentNode.parentNode; 97 | return matches ? parentNode : null; 98 | }, 99 | _dblbar: function (self, event) { 100 | self.dispatch('dblbar', { 101 | event: event, 102 | target: self._active() 103 | }); 104 | }, 105 | _dblclick: function (self, event) { 106 | self.dispatch('dblclick', { 107 | event: event, 108 | target: self._active() 109 | }); 110 | }, 111 | _dir: function () { 112 | return this.document.documentElement 113 | .getAttribute('dir') === 'rtl' ? -1 : 1; 114 | }, 115 | _dispatch: function (pos, event, target) { 116 | target.focus(); 117 | this._lastTarget = target; 118 | this.dispatch(pos < 0 ? 'prev' : 'next', { 119 | event: event, 120 | target: target 121 | }); 122 | }, 123 | _horizontal: function (event, pos) { 124 | var el = this._active(); 125 | var boundaries = this._closest(el, '.keys-bound'); 126 | if (boundaries) { 127 | var children = boundaries.querySelectorAll('.key'); 128 | if ( 129 | (el === children[0] && pos < 0) || 130 | (el === children[children.length - 1] && pos > 0) 131 | ) return; 132 | } 133 | this._dispatch(pos, event, this._target(pos)); 134 | }, 135 | _index: function (index, length) { 136 | if (index < 0) index = length - 1; 137 | else if (index === length) index = 0; 138 | return index; 139 | }, 140 | _onkeydown: function (event) { 141 | document.title = event.key; 142 | switch (event.key) { 143 | case 'ArrowDown': 144 | event.preventDefault(); 145 | this._vertical(event, 1); 146 | break; 147 | case 'ArrowUp': 148 | event.preventDefault(); 149 | this._vertical(event, -1); 150 | break; 151 | case 'ArrowRight': 152 | this._horizontal(event, 1 * this._dir()); 153 | break; 154 | case 'ArrowLeft': 155 | this._horizontal(event, -1 * this._dir()); 156 | break; 157 | case 'Backspace': 158 | this.dispatch('back', {event: event, target: this._active()}); 159 | break; 160 | case 'Escape': 161 | var target = this._active(); 162 | if (this.dispatch('esc', {event: event, target: target})) { 163 | this._lastTarget = null; 164 | target.blur(); 165 | } 166 | break; 167 | case 'Tab': 168 | setTimeout(this._tab, 0, this, event); 169 | break; 170 | case 'Enter': 171 | if (this.clickTimer) { 172 | clearTimeout(this.clickTimer); 173 | this.clickTimer = 0; 174 | setTimeout(this._dblclick, 125, this, event); 175 | } else { 176 | this.clickTimer = setTimeout(this._click, Navikeytor.delay, this, event); 177 | } 178 | break; 179 | case ' ': 180 | if (this.barTimer) { 181 | clearTimeout(this.barTimer); 182 | this.barTimer = 0; 183 | setTimeout(this._dblbar, 125, this, event); 184 | } else { 185 | this.barTimer = setTimeout(this._bar, Navikeytor.delay, this, event); 186 | } 187 | break; 188 | } 189 | }, 190 | _tab: function (self, event) { 191 | self.dispatch( 192 | event.shiftKey ? 'prev' : 'next', 193 | {event: event, target: self._active()} 194 | ); 195 | }, 196 | _target: function (pos) { 197 | return this.key[this._index( 198 | this._indexOf.call(this.key, this._active()) + pos, 199 | this.key.length 200 | )]; 201 | }, 202 | _touch: function (add) { 203 | var method = 'addEventListener'; 204 | var type = ['touchstart', 'touchmove', 'touchend']; 205 | if (add) { 206 | var self = this, timer = 0, sx = -1, sy = -1, x = 0, y = 0; 207 | this._touches = { 208 | auto: function (self) { 209 | var x = sx, y = sy; 210 | self.end(event); 211 | sx = x; 212 | sy = y; 213 | timer = setTimeout(auto, Navikeytor.delay / 2, self); 214 | }, 215 | handleEvent: function (event) { 216 | this[event.type === 'touchend' ? 'end' : 'move'](event); 217 | }, 218 | move: function (event) { 219 | var touch = event.touches[0]; 220 | if (sx < 0) { 221 | sx = x = touch.clientX; 222 | sy = y = touch.clientY; 223 | timer = setTimeout(auto, Navikeytor.delay / 2, this); 224 | } else { 225 | x = touch.clientX; 226 | y = touch.clientY; 227 | } 228 | }, 229 | end: function (event) { 230 | clearTimeout(timer); 231 | x = sx - x; 232 | y = sy - y; 233 | if (x) self._horizontal(event, x < 0 ? -1 : 1); 234 | else if (y) self._vertical(event, y < 0 ? -1 : 1); 235 | sx = sy = -1; 236 | } 237 | }; 238 | } else 239 | method = 'removeEventListener'; 240 | for (var i = 0; i < type.length; i++) 241 | this.el[method](type[i], this._touches, false); 242 | }, 243 | _vertical: function (event, pos) { 244 | var target; 245 | var current = this._active(); 246 | var container = this._closest(current, '.keys'); 247 | if (container) { 248 | var length = this.key.length; 249 | var i = this._indexOf.call(this.key, current); 250 | do { 251 | i += pos; 252 | target = this.key[this._index(i, length)]; 253 | } while (target !== current && container.contains(target)); 254 | this._dispatch(pos, event, target); 255 | } else { 256 | // TODO: double check this is cool 257 | // otherwise use: this._horizontal(event, pos); 258 | target = this._target(pos); 259 | var container = this._closest(target, '.keys'); 260 | if (container) target = container.querySelector('.key'); 261 | this._dispatch(pos, event, target); 262 | } 263 | } 264 | }; 265 | module.exports = Navikeytor; 266 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | /*! (c) 2018 Andrea Giammarchi (ISC) */ 2 | 3 | Navikeytor.prefix = 'navikeytor:'; 4 | Navikeytor.delay = 250; 5 | function Navikeytor(el) {"use strict"; 6 | this._lastTarget = null; 7 | this.el = el; 8 | this.barTimer = this.clickTimer = 0; 9 | this.document = el.ownerDocument || el; 10 | this.key = el.getElementsByClassName('key'); 11 | this.start(); 12 | } 13 | 14 | try { 15 | new CustomEvent(Navikeytor.prefix); 16 | Navikeytor.CE = CustomEvent; 17 | } catch(IE) { 18 | Navikeytor.CE = function (type, options) { 19 | var event = document.createEvent('CustomEvent'); 20 | event.initCustomEvent( 21 | type, 22 | !!options.bubbles, 23 | !!options.cancelable, 24 | options.detail 25 | ); 26 | return event; 27 | }; 28 | } 29 | 30 | Navikeytor.prototype = { 31 | constructor: Navikeytor, 32 | dispatch: function (type, detail) { 33 | return this.el.dispatchEvent(new Navikeytor.CE( 34 | Navikeytor.prefix + type, 35 | { 36 | detail: detail, 37 | bubbles: true 38 | } 39 | )); 40 | }, 41 | handleEvent: function (event) { 42 | this['_on' + event.type](event); 43 | }, 44 | off: function (type, listener) { 45 | this.el.removeEventListener(Navikeytor.prefix + type, listener, false); 46 | return this; 47 | }, 48 | on: function (type, listener) { 49 | this.el.addEventListener(Navikeytor.prefix + type, listener, false); 50 | return this; 51 | }, 52 | start: function () { 53 | this.el.addEventListener('keydown', this, false); 54 | if ('ontouchend' in this.el) 55 | this._touch(1); 56 | }, 57 | stop: function () { 58 | this.el.removeEventListener('keydown', this, false); 59 | clearTimeout(this.barTimer); 60 | clearTimeout(this.clickTimer); 61 | this.barTimer = this.clickTimer = 0; 62 | if ('ontouchend' in this.el) 63 | this._touch(0); 64 | }, 65 | _indexOf: [].indexOf, 66 | _active: function () { 67 | var body = this.document.body; 68 | var activeElement = this.document.activeElement || body; 69 | if (activeElement === body && this._lastTarget !== null) 70 | return this._lastTarget; 71 | return activeElement; 72 | }, 73 | _bar: function (self, event) { 74 | self.barTimer = 0; 75 | self.dispatch('bar', { 76 | event: event, 77 | target: self._active() 78 | }); 79 | }, 80 | _click: function (self, event) { 81 | self.clickTimer = 0; 82 | self.dispatch('click', { 83 | event: event, 84 | target: self._active() 85 | }); 86 | }, 87 | _closest: function (el, css) { 88 | return (el.closest || this._closestFix).call(el, css); 89 | }, 90 | _closestFix: function (css) { 91 | var parentNode = this, matches; 92 | while ( 93 | (matches = parentNode && parentNode.matches) && 94 | !parentNode.matches(css) 95 | ) 96 | parentNode = parentNode.parentNode; 97 | return matches ? parentNode : null; 98 | }, 99 | _dblbar: function (self, event) { 100 | self.dispatch('dblbar', { 101 | event: event, 102 | target: self._active() 103 | }); 104 | }, 105 | _dblclick: function (self, event) { 106 | self.dispatch('dblclick', { 107 | event: event, 108 | target: self._active() 109 | }); 110 | }, 111 | _dir: function () { 112 | return this.document.documentElement 113 | .getAttribute('dir') === 'rtl' ? -1 : 1; 114 | }, 115 | _dispatch: function (pos, event, target) { 116 | target.focus(); 117 | this._lastTarget = target; 118 | this.dispatch(pos < 0 ? 'prev' : 'next', { 119 | event: event, 120 | target: target 121 | }); 122 | }, 123 | _horizontal: function (event, pos) { 124 | var el = this._active(); 125 | var boundaries = this._closest(el, '.keys-bound'); 126 | if (boundaries) { 127 | var children = boundaries.querySelectorAll('.key'); 128 | if ( 129 | (el === children[0] && pos < 0) || 130 | (el === children[children.length - 1] && pos > 0) 131 | ) return; 132 | } 133 | this._dispatch(pos, event, this._target(pos)); 134 | }, 135 | _index: function (index, length) { 136 | if (index < 0) index = length - 1; 137 | else if (index === length) index = 0; 138 | return index; 139 | }, 140 | _onkeydown: function (event) { 141 | document.title = event.key; 142 | switch (event.key) { 143 | case 'ArrowDown': 144 | event.preventDefault(); 145 | this._vertical(event, 1); 146 | break; 147 | case 'ArrowUp': 148 | event.preventDefault(); 149 | this._vertical(event, -1); 150 | break; 151 | case 'ArrowRight': 152 | this._horizontal(event, 1 * this._dir()); 153 | break; 154 | case 'ArrowLeft': 155 | this._horizontal(event, -1 * this._dir()); 156 | break; 157 | case 'Backspace': 158 | this.dispatch('back', {event: event, target: this._active()}); 159 | break; 160 | case 'Escape': 161 | var target = this._active(); 162 | if (this.dispatch('esc', {event: event, target: target})) { 163 | this._lastTarget = null; 164 | target.blur(); 165 | } 166 | break; 167 | case 'Tab': 168 | setTimeout(this._tab, 0, this, event); 169 | break; 170 | case 'Enter': 171 | if (this.clickTimer) { 172 | clearTimeout(this.clickTimer); 173 | this.clickTimer = 0; 174 | setTimeout(this._dblclick, 125, this, event); 175 | } else { 176 | this.clickTimer = setTimeout(this._click, Navikeytor.delay, this, event); 177 | } 178 | break; 179 | case ' ': 180 | if (this.barTimer) { 181 | clearTimeout(this.barTimer); 182 | this.barTimer = 0; 183 | setTimeout(this._dblbar, 125, this, event); 184 | } else { 185 | this.barTimer = setTimeout(this._bar, Navikeytor.delay, this, event); 186 | } 187 | break; 188 | } 189 | }, 190 | _tab: function (self, event) { 191 | self.dispatch( 192 | event.shiftKey ? 'prev' : 'next', 193 | {event: event, target: self._active()} 194 | ); 195 | }, 196 | _target: function (pos) { 197 | return this.key[this._index( 198 | this._indexOf.call(this.key, this._active()) + pos, 199 | this.key.length 200 | )]; 201 | }, 202 | _touch: function (add) { 203 | var method = 'addEventListener'; 204 | var type = ['touchstart', 'touchmove', 'touchend']; 205 | if (add) { 206 | var self = this, timer = 0, sx = -1, sy = -1, x = 0, y = 0; 207 | this._touches = { 208 | auto: function (self) { 209 | var x = sx, y = sy; 210 | self.end(event); 211 | sx = x; 212 | sy = y; 213 | timer = setTimeout(auto, Navikeytor.delay / 2, self); 214 | }, 215 | handleEvent: function (event) { 216 | this[event.type === 'touchend' ? 'end' : 'move'](event); 217 | }, 218 | move: function (event) { 219 | var touch = event.touches[0]; 220 | if (sx < 0) { 221 | sx = x = touch.clientX; 222 | sy = y = touch.clientY; 223 | timer = setTimeout(auto, Navikeytor.delay / 2, this); 224 | } else { 225 | x = touch.clientX; 226 | y = touch.clientY; 227 | } 228 | }, 229 | end: function (event) { 230 | clearTimeout(timer); 231 | x = sx - x; 232 | y = sy - y; 233 | if (x) self._horizontal(event, x < 0 ? -1 : 1); 234 | else if (y) self._vertical(event, y < 0 ? -1 : 1); 235 | sx = sy = -1; 236 | } 237 | }; 238 | } else 239 | method = 'removeEventListener'; 240 | for (var i = 0; i < type.length; i++) 241 | this.el[method](type[i], this._touches, false); 242 | }, 243 | _vertical: function (event, pos) { 244 | var target; 245 | var current = this._active(); 246 | var container = this._closest(current, '.keys'); 247 | if (container) { 248 | var length = this.key.length; 249 | var i = this._indexOf.call(this.key, current); 250 | do { 251 | i += pos; 252 | target = this.key[this._index(i, length)]; 253 | } while (target !== current && container.contains(target)); 254 | this._dispatch(pos, event, target); 255 | } else { 256 | // TODO: double check this is cool 257 | // otherwise use: this._horizontal(event, pos); 258 | target = this._target(pos); 259 | var container = this._closest(target, '.keys'); 260 | if (container) target = container.querySelector('.key'); 261 | this._dispatch(pos, event, target); 262 | } 263 | } 264 | }; 265 | export default Navikeytor; 266 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! (c) 2018 Andrea Giammarchi (ISC) */ 2 | 3 | Navikeytor.prefix = 'navikeytor:'; 4 | Navikeytor.delay = 250; 5 | function Navikeytor(el) {"use strict"; 6 | this._lastTarget = null; 7 | this.el = el; 8 | this.barTimer = this.clickTimer = 0; 9 | this.document = el.ownerDocument || el; 10 | this.key = el.getElementsByClassName('key'); 11 | this.start(); 12 | } 13 | 14 | try { 15 | new CustomEvent(Navikeytor.prefix); 16 | Navikeytor.CE = CustomEvent; 17 | } catch(IE) { 18 | Navikeytor.CE = function (type, options) { 19 | var event = document.createEvent('CustomEvent'); 20 | event.initCustomEvent( 21 | type, 22 | !!options.bubbles, 23 | !!options.cancelable, 24 | options.detail 25 | ); 26 | return event; 27 | }; 28 | } 29 | 30 | Navikeytor.prototype = { 31 | constructor: Navikeytor, 32 | dispatch: function (type, detail) { 33 | return this.el.dispatchEvent(new Navikeytor.CE( 34 | Navikeytor.prefix + type, 35 | { 36 | detail: detail, 37 | bubbles: true 38 | } 39 | )); 40 | }, 41 | handleEvent: function (event) { 42 | this['_on' + event.type](event); 43 | }, 44 | off: function (type, listener) { 45 | this.el.removeEventListener(Navikeytor.prefix + type, listener, false); 46 | return this; 47 | }, 48 | on: function (type, listener) { 49 | this.el.addEventListener(Navikeytor.prefix + type, listener, false); 50 | return this; 51 | }, 52 | start: function () { 53 | this.el.addEventListener('keydown', this, false); 54 | if ('ontouchend' in this.el) 55 | this._touch(1); 56 | }, 57 | stop: function () { 58 | this.el.removeEventListener('keydown', this, false); 59 | clearTimeout(this.barTimer); 60 | clearTimeout(this.clickTimer); 61 | this.barTimer = this.clickTimer = 0; 62 | if ('ontouchend' in this.el) 63 | this._touch(0); 64 | }, 65 | _indexOf: [].indexOf, 66 | _active: function () { 67 | var body = this.document.body; 68 | var activeElement = this.document.activeElement || body; 69 | if (activeElement === body && this._lastTarget !== null) 70 | return this._lastTarget; 71 | return activeElement; 72 | }, 73 | _bar: function (self, event) { 74 | self.barTimer = 0; 75 | self.dispatch('bar', { 76 | event: event, 77 | target: self._active() 78 | }); 79 | }, 80 | _click: function (self, event) { 81 | self.clickTimer = 0; 82 | self.dispatch('click', { 83 | event: event, 84 | target: self._active() 85 | }); 86 | }, 87 | _closest: function (el, css) { 88 | return (el.closest || this._closestFix).call(el, css); 89 | }, 90 | _closestFix: function (css) { 91 | var parentNode = this, matches; 92 | while ( 93 | (matches = parentNode && parentNode.matches) && 94 | !parentNode.matches(css) 95 | ) 96 | parentNode = parentNode.parentNode; 97 | return matches ? parentNode : null; 98 | }, 99 | _dblbar: function (self, event) { 100 | self.dispatch('dblbar', { 101 | event: event, 102 | target: self._active() 103 | }); 104 | }, 105 | _dblclick: function (self, event) { 106 | self.dispatch('dblclick', { 107 | event: event, 108 | target: self._active() 109 | }); 110 | }, 111 | _dir: function () { 112 | return this.document.documentElement 113 | .getAttribute('dir') === 'rtl' ? -1 : 1; 114 | }, 115 | _dispatch: function (pos, event, target) { 116 | target.focus(); 117 | this._lastTarget = target; 118 | this.dispatch(pos < 0 ? 'prev' : 'next', { 119 | event: event, 120 | target: target 121 | }); 122 | }, 123 | _horizontal: function (event, pos) { 124 | var el = this._active(); 125 | var boundaries = this._closest(el, '.keys-bound'); 126 | if (boundaries) { 127 | var children = boundaries.querySelectorAll('.key'); 128 | if ( 129 | (el === children[0] && pos < 0) || 130 | (el === children[children.length - 1] && pos > 0) 131 | ) return; 132 | } 133 | this._dispatch(pos, event, this._target(pos)); 134 | }, 135 | _index: function (index, length) { 136 | if (index < 0) index = length - 1; 137 | else if (index === length) index = 0; 138 | return index; 139 | }, 140 | _onkeydown: function (event) { 141 | document.title = event.key; 142 | switch (event.key) { 143 | case 'ArrowDown': 144 | event.preventDefault(); 145 | this._vertical(event, 1); 146 | break; 147 | case 'ArrowUp': 148 | event.preventDefault(); 149 | this._vertical(event, -1); 150 | break; 151 | case 'ArrowRight': 152 | this._horizontal(event, 1 * this._dir()); 153 | break; 154 | case 'ArrowLeft': 155 | this._horizontal(event, -1 * this._dir()); 156 | break; 157 | case 'Backspace': 158 | this.dispatch('back', {event: event, target: this._active()}); 159 | break; 160 | case 'Escape': 161 | var target = this._active(); 162 | if (this.dispatch('esc', {event: event, target: target})) { 163 | this._lastTarget = null; 164 | target.blur(); 165 | } 166 | break; 167 | case 'Tab': 168 | setTimeout(this._tab, 0, this, event); 169 | break; 170 | case 'Enter': 171 | if (this.clickTimer) { 172 | clearTimeout(this.clickTimer); 173 | this.clickTimer = 0; 174 | setTimeout(this._dblclick, 125, this, event); 175 | } else { 176 | this.clickTimer = setTimeout(this._click, Navikeytor.delay, this, event); 177 | } 178 | break; 179 | case ' ': 180 | if (this.barTimer) { 181 | clearTimeout(this.barTimer); 182 | this.barTimer = 0; 183 | setTimeout(this._dblbar, 125, this, event); 184 | } else { 185 | this.barTimer = setTimeout(this._bar, Navikeytor.delay, this, event); 186 | } 187 | break; 188 | } 189 | }, 190 | _tab: function (self, event) { 191 | self.dispatch( 192 | event.shiftKey ? 'prev' : 'next', 193 | {event: event, target: self._active()} 194 | ); 195 | }, 196 | _target: function (pos) { 197 | return this.key[this._index( 198 | this._indexOf.call(this.key, this._active()) + pos, 199 | this.key.length 200 | )]; 201 | }, 202 | _touch: function (add) { 203 | var method = 'addEventListener'; 204 | var type = ['touchstart', 'touchmove', 'touchend']; 205 | if (add) { 206 | var self = this, timer = 0, sx = -1, sy = -1, x = 0, y = 0; 207 | this._touches = { 208 | auto: function (self) { 209 | var x = sx, y = sy; 210 | self.end(event); 211 | sx = x; 212 | sy = y; 213 | timer = setTimeout(auto, Navikeytor.delay / 2, self); 214 | }, 215 | handleEvent: function (event) { 216 | this[event.type === 'touchend' ? 'end' : 'move'](event); 217 | }, 218 | move: function (event) { 219 | var touch = event.touches[0]; 220 | if (sx < 0) { 221 | sx = x = touch.clientX; 222 | sy = y = touch.clientY; 223 | timer = setTimeout(auto, Navikeytor.delay / 2, this); 224 | } else { 225 | x = touch.clientX; 226 | y = touch.clientY; 227 | } 228 | }, 229 | end: function (event) { 230 | clearTimeout(timer); 231 | x = sx - x; 232 | y = sy - y; 233 | if (x) self._horizontal(event, x < 0 ? -1 : 1); 234 | else if (y) self._vertical(event, y < 0 ? -1 : 1); 235 | sx = sy = -1; 236 | } 237 | }; 238 | } else 239 | method = 'removeEventListener'; 240 | for (var i = 0; i < type.length; i++) 241 | this.el[method](type[i], this._touches, false); 242 | }, 243 | _vertical: function (event, pos) { 244 | var target; 245 | var current = this._active(); 246 | var container = this._closest(current, '.keys'); 247 | if (container) { 248 | var length = this.key.length; 249 | var i = this._indexOf.call(this.key, current); 250 | do { 251 | i += pos; 252 | target = this.key[this._index(i, length)]; 253 | } while (target !== current && container.contains(target)); 254 | this._dispatch(pos, event, target); 255 | } else { 256 | // TODO: double check this is cool 257 | // otherwise use: this._horizontal(event, pos); 258 | target = this._target(pos); 259 | var container = this._closest(target, '.keys'); 260 | if (container) target = container.querySelector('.key'); 261 | this._dispatch(pos, event, target); 262 | } 263 | } 264 | }; 265 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "navikeytor", 3 | "version": "0.0.4", 4 | "description": "100% keyboard based spatial navigation.", 5 | "module": "esm/index.js", 6 | "main": "cjs/index.js", 7 | "unpkg": "index.js", 8 | "scripts": { 9 | "build": "npm run cjs && npm run esm && npm run test:js", 10 | "cjs": "cp index.js cjs/ && echo 'module.exports = Navikeytor;' >> cjs/index.js", 11 | "esm": "cp index.js esm/ && echo 'export default Navikeytor;' >> esm/index.js", 12 | "test:js": "cp index.js test/js/navikeytor.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/WebReflection/navikeytor.git" 17 | }, 18 | "keywords": [ 19 | "key", 20 | "spatial", 21 | "navigation", 22 | "keyboard" 23 | ], 24 | "author": "Andrea Giammarchi", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/WebReflection/navikeytor/issues" 28 | }, 29 | "homepage": "https://github.com/WebReflection/navikeytor#readme" 30 | } 31 | -------------------------------------------------------------------------------- /test/css/index.css: -------------------------------------------------------------------------------- 1 | /*! (c) 2018 Andrea Giammarchi (ISC) */ 2 | 3 | html { 4 | background-color: white; 5 | color: black; 6 | } 7 | 8 | header { 9 | border-bottom: 1px solid silver; 10 | } 11 | 12 | /* /theme */ 13 | 14 | html { 15 | -webkit-box-sizing: border-box; 16 | -moz-box-sizing: border-box; 17 | box-sizing: border-box; 18 | } 19 | 20 | *, *:before, *:after{ 21 | -webkit-box-sizing: inherit; 22 | -moz-box-sizing: inherit; 23 | box-sizing: inherit; 24 | /* 25 | cursor: none; 26 | pointer-events: none; 27 | */ 28 | } 29 | 30 | *:focus:not(input):not(select) { 31 | outline: none; 32 | background: #F5F5F5; 33 | } 34 | 35 | html, body { 36 | margin: 0; 37 | padding: 0; 38 | } 39 | 40 | body { 41 | display: flex; 42 | flex-direction: column; 43 | position: absolute; 44 | top: 0; 45 | right: 0; 46 | bottom: 0; 47 | left: 0; 48 | } 49 | 50 | header { 51 | display: block; 52 | width: 100%; 53 | } 54 | 55 | nav, nav * { 56 | padding: 0; 57 | margin: 0; 58 | list-style: none; 59 | } 60 | 61 | nav ul { 62 | display: flex; 63 | flex-direction: row; 64 | } 65 | 66 | nav ul li { 67 | flex-grow: 1; 68 | text-align: center; 69 | } 70 | 71 | nav ul li a { 72 | display: inline-block; 73 | padding: 8px; 74 | width: 100%; 75 | text-decoration: none; 76 | color: transparent; 77 | text-shadow: 0 0 #1da1f2; 78 | } 79 | 80 | main { 81 | flex-grow: 1; 82 | overflow: auto; 83 | } 84 | 85 | main > button { 86 | text-align: left; 87 | -webkit-appearance: none; 88 | -moz-appearance: none; 89 | appearance: none; 90 | border: 0; 91 | display: block; 92 | width: 100%; 93 | background: none; 94 | } 95 | 96 | html[dir="rtl"] main > button { 97 | text-align: right; 98 | } -------------------------------------------------------------------------------- /test/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/navikeytor/7542a8078892d716ca07ab7324a0ef179733c4c9/test/favicon.ico -------------------------------------------------------------------------------- /test/img/tweety_112.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/navikeytor/7542a8078892d716ca07ab7324a0ef179733c4c9/test/img/tweety_112.png -------------------------------------------------------------------------------- /test/img/tweety_56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/navikeytor/7542a8078892d716ca07ab7324a0ef179733c4c9/test/img/tweety_56.png -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |