├── .gitignore ├── README.md ├── index.js ├── lib ├── index.js └── spatial_navigation.js ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-js-spatial-navigation [![npm version](http://img.shields.io/npm/v/vue-js-spatial-navigation.svg?style=flat)](https://npmjs.org/package/vue-js-spatial-navigation "View this project on npm") 2 | 3 | Vue directive of [js-spatial-navigation](https://github.com/luke-chang/js-spatial-navigation); 4 | 5 | ## Installation 6 | 7 | ### NPM 8 | 9 | ```shell 10 | npm install vue-js-spatial-navigation 11 | ``` 12 | 13 | ## Getting Started 14 | 15 | ```javascript 16 | import Vue from "vue"; 17 | import vjsn from "vue-js-spatial-navigation"; 18 | 19 | Vue.use(vjsn); 20 | ``` 21 | 22 | #### Optional global [Configuration](https://github.com/luke-chang/js-spatial-navigation#configuration) 23 | 24 | #### Additional configuration `scrollOptions`: 25 | 26 | - The page will auto scroll to the focus element by using [`scrollIntoView`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView). 27 | 28 | - You can set this `scrollOptions` for the `scrollIntoViewOptions`. 29 | 30 | - The page will not scroll to the focus element when setting `scrollOptions` to `""` or `null`. 31 | 32 | ```javascript 33 | const config = { 34 | straightOnly: false, 35 | straightOverlapThreshold: 0.5, 36 | rememberSource: false, 37 | disabled: false, 38 | defaultElement: "", 39 | enterTo: "", 40 | leaveFor: null, 41 | restrict: "self-first", 42 | tabIndexIgnoreList: "a, input, select, textarea, button, iframe, [contentEditable=true]", 43 | navigableFilter: null, 44 | scrollOptions: { behavior: "smooth", block: "center" } 45 | }; 46 | Vue.use(vjsn, config); 47 | ``` 48 | 49 | ## Documentation 50 | 51 | ### `$SpatialNavigation` 52 | 53 | A global Vue instance property for [SpatialNavigation](https://github.com/luke-chang/js-spatial-navigation#api-reference); 54 | 55 | ```javascript 56 | // you can access SpatialNavigation in every instance 57 | this.$SpatialNavigation; 58 | ``` 59 | 60 | ### `v-focus` 61 | 62 | A directive that make the element focusable. 63 | 64 | The element with `v-focus` must under the element with `v-focus-section`, see [v-focus-section](#v-focus-section) 65 | 66 | ```html 67 |
68 |
69 |
70 | ``` 71 | 72 | #### dynamic control 73 | 74 | ```html 75 |
76 |
77 |
78 | 79 | 93 | ``` 94 | 95 | ### `v-focus-section` 96 | 97 | A directive that define a focus [Section](https://github.com/luke-chang/js-spatial-navigation#spatialnavigationaddsectionid-config) 98 | 99 | ```html 100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | ``` 109 | 110 | #### Pass a specified section id 111 | 112 | ```html 113 | 114 |
115 |
116 |
117 | ``` 118 | 119 | #### Set configuration 120 | 121 | ```html 122 | 123 |
124 |
125 |
126 | ``` 127 | 128 | #### Set default section 129 | 130 | ```html 131 | 132 |
133 |
134 |
135 | ``` 136 | 137 | ### `v-disable-focus-section` 138 | 139 | This directive will make the conponemt unnavigable. 140 | See [SpatialNavigation.disable()](https://github.com/luke-chang/js-spatial-navigation#spatialnavigationdisablesectionid), 141 | [SpatialNavigation.enable()](https://github.com/luke-chang/js-spatial-navigation#spatialnavigationenablesectionid). 142 | 143 | ```html 144 |
145 |
146 |
147 | 148 | 162 | ``` 163 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib') -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import SpatialNavigation from "./spatial_navigation.js"; 2 | import "focus-options-polyfill"; 3 | import "scroll-behavior-polyfill"; 4 | 5 | const vueSpatialNavigation = { 6 | install(Vue, config) { 7 | const globalConfig = { 8 | selector: `[data-focusable=true]` 9 | }; 10 | Object.assign(globalConfig, config); 11 | SpatialNavigation.init(); 12 | SpatialNavigation.set(globalConfig); 13 | Vue.prototype.$SpatialNavigation = SpatialNavigation; 14 | 15 | const assignConfig = (sectionId, config) => { 16 | let sectionConfig = Object.assign({}, globalConfig); 17 | if (config) { 18 | Object.assign(sectionConfig, config); 19 | } 20 | sectionConfig.selector = `[data-section-id="${sectionId}"] [data-focusable=true]`; 21 | return sectionConfig; 22 | }; 23 | 24 | // focus section directive 25 | Vue.directive("focus-section", { 26 | bind(el, binding) { 27 | let sectionId = null; 28 | if (binding.arg) { 29 | sectionId = binding.arg; 30 | try { 31 | SpatialNavigation.add(sectionId, {}); 32 | } catch (error) {} 33 | } else { 34 | sectionId = SpatialNavigation.add({}); 35 | } 36 | 37 | // set sectionid to data set for removing when unbinding 38 | el.dataset.sectionId = sectionId; 39 | SpatialNavigation.set(sectionId, assignConfig(sectionId, binding.value)); 40 | // set default section 41 | if (binding.modifiers.default) { 42 | SpatialNavigation.setDefaultSection(sectionId); 43 | } 44 | }, 45 | update(el, binding) { 46 | let sectionId = el.dataset.sectionId; 47 | if (binding.arg && sectionId != binding.arg) { 48 | sectionId = binding.arg; 49 | el.dataset.sectionId = sectionId; 50 | } 51 | if (binding.value) { 52 | try { 53 | SpatialNavigation.set(sectionId, binding.value); 54 | } catch (error) { 55 | SpatialNavigation.add(sectionId, assignConfig(sectionId, binding.value)); 56 | } 57 | } 58 | }, 59 | unbind(el) { 60 | SpatialNavigation.remove(el.dataset.sectionId); 61 | } 62 | }); 63 | 64 | const disableSection = (sectionId, disable) => { 65 | if (disable == false) { 66 | SpatialNavigation.enable(sectionId); 67 | } else { 68 | SpatialNavigation.disable(sectionId); 69 | } 70 | }; 71 | // diasble focus section directive 72 | Vue.directive("disable-focus-section", { 73 | bind(el, binding) { 74 | disableSection(el.dataset.sectionId, binding.value); 75 | }, 76 | update(el, binding) { 77 | disableSection(el.dataset.sectionId, binding.value); 78 | } 79 | }); 80 | 81 | const disableElement = (el, focusable) => { 82 | focusable = focusable == false ? false : true; 83 | if (!el.dataset.focusable || el.dataset.focusable != focusable + "") { 84 | el.dataset.focusable = focusable; 85 | if (focusable) el.tabIndex = -1; 86 | } 87 | }; 88 | // focusable directive 89 | Vue.directive("focus", { 90 | bind(el, binding) { 91 | disableElement(el, binding.value); 92 | }, 93 | update(el, binding) { 94 | disableElement(el, binding.value); 95 | }, 96 | unbind(el) { 97 | el.removeAttribute("data-focusable"); 98 | } 99 | }); 100 | } 101 | }; 102 | 103 | export default vueSpatialNavigation; 104 | -------------------------------------------------------------------------------- /lib/spatial_navigation.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * A javascript-based implementation of Spatial Navigation. 4 | * 5 | * Copyright (c) 2017 Luke Chang. 6 | * https://github.com/luke-chang/js-spatial-navigation 7 | * 8 | * Licensed under the MPL 2.0. 9 | */ 10 | ;(function($) { 11 | 'use strict'; 12 | 13 | /************************/ 14 | /* Global Configuration */ 15 | /************************/ 16 | // Note: an can be one of following types: 17 | // - a valid selector string for "querySelectorAll" or jQuery (if it exists) 18 | // - a NodeList or an array containing DOM elements 19 | // - a single DOM element 20 | // - a jQuery object 21 | // - a string "@" to indicate the specified section 22 | // - a string "@" to indicate the default section 23 | var GlobalConfig = { 24 | selector: "", // can be a valid except "@" syntax. 25 | straightOnly: false, 26 | straightOverlapThreshold: 0.5, 27 | rememberSource: false, 28 | disabled: false, 29 | defaultElement: "", // except "@" syntax. 30 | enterTo: "", // '', 'last-focused', 'default-element' 31 | leaveFor: null, // {left: , right: , 32 | // up: , down: } 33 | restrict: "self-first", // 'self-first', 'self-only', 'none' 34 | tabIndexIgnoreList: "a, input, select, textarea, button, iframe, [contentEditable=true]", 35 | navigableFilter: null, 36 | scrollOptions: null // scrollIntoViewOptions https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView 37 | }; 38 | 39 | /*********************/ 40 | /* Constant Variable */ 41 | /*********************/ 42 | var KEYMAPPING = { 43 | '37': 'left', 44 | '38': 'up', 45 | '39': 'right', 46 | '40': 'down' 47 | }; 48 | 49 | var REVERSE = { 50 | 'left': 'right', 51 | 'up': 'down', 52 | 'right': 'left', 53 | 'down': 'up' 54 | }; 55 | 56 | var EVENT_PREFIX = 'sn:'; 57 | var ID_POOL_PREFIX = 'section-'; 58 | 59 | /********************/ 60 | /* Private Variable */ 61 | /********************/ 62 | var _idPool = 0; 63 | var _ready = false; 64 | var _pause = false; 65 | var _sections = {}; 66 | var _sectionCount = 0; 67 | var _defaultSectionId = ''; 68 | var _lastSectionId = ''; 69 | var _duringFocusChange = false; 70 | 71 | /************/ 72 | /* Polyfill */ 73 | /************/ 74 | var elementMatchesSelector = 75 | Element.prototype.matches || 76 | Element.prototype.matchesSelector || 77 | Element.prototype.mozMatchesSelector || 78 | Element.prototype.webkitMatchesSelector || 79 | Element.prototype.msMatchesSelector || 80 | Element.prototype.oMatchesSelector || 81 | function (selector) { 82 | var matchedNodes = 83 | (this.parentNode || this.document).querySelectorAll(selector); 84 | return [].slice.call(matchedNodes).indexOf(this) >= 0; 85 | }; 86 | 87 | /*****************/ 88 | /* Core Function */ 89 | /*****************/ 90 | function getRect(elem) { 91 | var cr = elem.getBoundingClientRect(); 92 | var rect = { 93 | left: cr.left, 94 | top: cr.top, 95 | right: cr.right, 96 | bottom: cr.bottom, 97 | width: cr.width, 98 | height: cr.height 99 | }; 100 | rect.element = elem; 101 | rect.center = { 102 | x: rect.left + Math.floor(rect.width / 2), 103 | y: rect.top + Math.floor(rect.height / 2) 104 | }; 105 | rect.center.left = rect.center.right = rect.center.x; 106 | rect.center.top = rect.center.bottom = rect.center.y; 107 | return rect; 108 | } 109 | 110 | function partition(rects, targetRect, straightOverlapThreshold) { 111 | var groups = [[], [], [], [], [], [], [], [], []]; 112 | 113 | for (var i = 0; i < rects.length; i++) { 114 | var rect = rects[i]; 115 | var center = rect.center; 116 | var x, y, groupId; 117 | 118 | if (center.x < targetRect.left) { 119 | x = 0; 120 | } else if (center.x <= targetRect.right) { 121 | x = 1; 122 | } else { 123 | x = 2; 124 | } 125 | 126 | if (center.y < targetRect.top) { 127 | y = 0; 128 | } else if (center.y <= targetRect.bottom) { 129 | y = 1; 130 | } else { 131 | y = 2; 132 | } 133 | 134 | groupId = y * 3 + x; 135 | groups[groupId].push(rect); 136 | 137 | if ([0, 2, 6, 8].indexOf(groupId) !== -1) { 138 | var threshold = straightOverlapThreshold; 139 | 140 | if (rect.left <= targetRect.right - targetRect.width * threshold) { 141 | if (groupId === 2) { 142 | groups[1].push(rect); 143 | } else if (groupId === 8) { 144 | groups[7].push(rect); 145 | } 146 | } 147 | 148 | if (rect.right >= targetRect.left + targetRect.width * threshold) { 149 | if (groupId === 0) { 150 | groups[1].push(rect); 151 | } else if (groupId === 6) { 152 | groups[7].push(rect); 153 | } 154 | } 155 | 156 | if (rect.top <= targetRect.bottom - targetRect.height * threshold) { 157 | if (groupId === 6) { 158 | groups[3].push(rect); 159 | } else if (groupId === 8) { 160 | groups[5].push(rect); 161 | } 162 | } 163 | 164 | if (rect.bottom >= targetRect.top + targetRect.height * threshold) { 165 | if (groupId === 0) { 166 | groups[3].push(rect); 167 | } else if (groupId === 2) { 168 | groups[5].push(rect); 169 | } 170 | } 171 | } 172 | } 173 | 174 | return groups; 175 | } 176 | 177 | function generateDistanceFunction(targetRect) { 178 | return { 179 | nearPlumbLineIsBetter: function(rect) { 180 | var d; 181 | if (rect.center.x < targetRect.center.x) { 182 | d = targetRect.center.x - rect.right; 183 | } else { 184 | d = rect.left - targetRect.center.x; 185 | } 186 | return d < 0 ? 0 : d; 187 | }, 188 | nearHorizonIsBetter: function(rect) { 189 | var d; 190 | if (rect.center.y < targetRect.center.y) { 191 | d = targetRect.center.y - rect.bottom; 192 | } else { 193 | d = rect.top - targetRect.center.y; 194 | } 195 | return d < 0 ? 0 : d; 196 | }, 197 | nearTargetLeftIsBetter: function(rect) { 198 | var d; 199 | if (rect.center.x < targetRect.center.x) { 200 | d = targetRect.left - rect.right; 201 | } else { 202 | d = rect.left - targetRect.left; 203 | } 204 | return d < 0 ? 0 : d; 205 | }, 206 | nearTargetTopIsBetter: function(rect) { 207 | var d; 208 | if (rect.center.y < targetRect.center.y) { 209 | d = targetRect.top - rect.bottom; 210 | } else { 211 | d = rect.top - targetRect.top; 212 | } 213 | return d < 0 ? 0 : d; 214 | }, 215 | topIsBetter: function(rect) { 216 | return rect.top; 217 | }, 218 | bottomIsBetter: function(rect) { 219 | return -1 * rect.bottom; 220 | }, 221 | leftIsBetter: function(rect) { 222 | return rect.left; 223 | }, 224 | rightIsBetter: function(rect) { 225 | return -1 * rect.right; 226 | } 227 | }; 228 | } 229 | 230 | function prioritize(priorities) { 231 | var destPriority = null; 232 | for (var i = 0; i < priorities.length; i++) { 233 | if (priorities[i].group.length) { 234 | destPriority = priorities[i]; 235 | break; 236 | } 237 | } 238 | 239 | if (!destPriority) { 240 | return null; 241 | } 242 | 243 | var destDistance = destPriority.distance; 244 | 245 | destPriority.group.sort(function(a, b) { 246 | for (var i = 0; i < destDistance.length; i++) { 247 | var distance = destDistance[i]; 248 | var delta = distance(a) - distance(b); 249 | if (delta) { 250 | return delta; 251 | } 252 | } 253 | return 0; 254 | }); 255 | 256 | return destPriority.group; 257 | } 258 | 259 | function navigate(target, direction, candidates, config) { 260 | if (!target || !direction || !candidates || !candidates.length) { 261 | return null; 262 | } 263 | 264 | var rects = []; 265 | for (var i = 0; i < candidates.length; i++) { 266 | var rect = getRect(candidates[i]); 267 | if (rect) { 268 | rects.push(rect); 269 | } 270 | } 271 | if (!rects.length) { 272 | return null; 273 | } 274 | 275 | var targetRect = getRect(target); 276 | if (!targetRect) { 277 | return null; 278 | } 279 | 280 | var distanceFunction = generateDistanceFunction(targetRect); 281 | 282 | var groups = partition( 283 | rects, 284 | targetRect, 285 | config.straightOverlapThreshold 286 | ); 287 | 288 | var internalGroups = partition( 289 | groups[4], 290 | targetRect.center, 291 | config.straightOverlapThreshold 292 | ); 293 | 294 | var priorities; 295 | 296 | switch (direction) { 297 | case 'left': 298 | priorities = [ 299 | { 300 | group: internalGroups[0].concat(internalGroups[3]) 301 | .concat(internalGroups[6]), 302 | distance: [ 303 | distanceFunction.nearPlumbLineIsBetter, 304 | distanceFunction.topIsBetter 305 | ] 306 | }, 307 | { 308 | group: groups[3], 309 | distance: [ 310 | distanceFunction.nearPlumbLineIsBetter, 311 | distanceFunction.topIsBetter 312 | ] 313 | }, 314 | { 315 | group: groups[0].concat(groups[6]), 316 | distance: [ 317 | distanceFunction.nearHorizonIsBetter, 318 | distanceFunction.rightIsBetter, 319 | distanceFunction.nearTargetTopIsBetter 320 | ] 321 | } 322 | ]; 323 | break; 324 | case 'right': 325 | priorities = [ 326 | { 327 | group: internalGroups[2].concat(internalGroups[5]) 328 | .concat(internalGroups[8]), 329 | distance: [ 330 | distanceFunction.nearPlumbLineIsBetter, 331 | distanceFunction.topIsBetter 332 | ] 333 | }, 334 | { 335 | group: groups[5], 336 | distance: [ 337 | distanceFunction.nearPlumbLineIsBetter, 338 | distanceFunction.topIsBetter 339 | ] 340 | }, 341 | { 342 | group: groups[2].concat(groups[8]), 343 | distance: [ 344 | distanceFunction.nearHorizonIsBetter, 345 | distanceFunction.leftIsBetter, 346 | distanceFunction.nearTargetTopIsBetter 347 | ] 348 | } 349 | ]; 350 | break; 351 | case 'up': 352 | priorities = [ 353 | { 354 | group: internalGroups[0].concat(internalGroups[1]) 355 | .concat(internalGroups[2]), 356 | distance: [ 357 | distanceFunction.nearHorizonIsBetter, 358 | distanceFunction.leftIsBetter 359 | ] 360 | }, 361 | { 362 | group: groups[1], 363 | distance: [ 364 | distanceFunction.nearHorizonIsBetter, 365 | distanceFunction.leftIsBetter 366 | ] 367 | }, 368 | { 369 | group: groups[0].concat(groups[2]), 370 | distance: [ 371 | distanceFunction.nearPlumbLineIsBetter, 372 | distanceFunction.bottomIsBetter, 373 | distanceFunction.nearTargetLeftIsBetter 374 | ] 375 | } 376 | ]; 377 | break; 378 | case 'down': 379 | priorities = [ 380 | { 381 | group: internalGroups[6].concat(internalGroups[7]) 382 | .concat(internalGroups[8]), 383 | distance: [ 384 | distanceFunction.nearHorizonIsBetter, 385 | distanceFunction.leftIsBetter 386 | ] 387 | }, 388 | { 389 | group: groups[7], 390 | distance: [ 391 | distanceFunction.nearHorizonIsBetter, 392 | distanceFunction.leftIsBetter 393 | ] 394 | }, 395 | { 396 | group: groups[6].concat(groups[8]), 397 | distance: [ 398 | distanceFunction.nearPlumbLineIsBetter, 399 | distanceFunction.topIsBetter, 400 | distanceFunction.nearTargetLeftIsBetter 401 | ] 402 | } 403 | ]; 404 | break; 405 | default: 406 | return null; 407 | } 408 | 409 | if (config.straightOnly) { 410 | priorities.pop(); 411 | } 412 | 413 | var destGroup = prioritize(priorities); 414 | if (!destGroup) { 415 | return null; 416 | } 417 | 418 | var dest = null; 419 | if (config.rememberSource && 420 | config.previous && 421 | config.previous.destination === target && 422 | config.previous.reverse === direction) { 423 | for (var j = 0; j < destGroup.length; j++) { 424 | if (destGroup[j].element === config.previous.target) { 425 | dest = destGroup[j].element; 426 | break; 427 | } 428 | } 429 | } 430 | 431 | if (!dest) { 432 | dest = destGroup[0].element; 433 | } 434 | 435 | return dest; 436 | } 437 | 438 | /********************/ 439 | /* Private Function */ 440 | /********************/ 441 | function generateId() { 442 | var id; 443 | while(true) { 444 | id = ID_POOL_PREFIX + String(++_idPool); 445 | if (!_sections[id]) { 446 | break; 447 | } 448 | } 449 | return id; 450 | } 451 | 452 | function parseSelector(selector) { 453 | var result; 454 | if ($) { 455 | result = $(selector).get(); 456 | } else if (typeof selector === 'string') { 457 | result = [].slice.call(document.querySelectorAll(selector)); 458 | } else if (typeof selector === 'object' && selector.length) { 459 | result = [].slice.call(selector); 460 | } else if (typeof selector === 'object' && selector.nodeType === 1) { 461 | result = [selector]; 462 | } else { 463 | result = []; 464 | } 465 | return result; 466 | } 467 | 468 | function matchSelector(elem, selector) { 469 | if ($) { 470 | return $(elem).is(selector); 471 | } else if (typeof selector === 'string') { 472 | return elementMatchesSelector.call(elem, selector); 473 | } else if (typeof selector === 'object' && selector.length) { 474 | return selector.indexOf(elem) >= 0; 475 | } else if (typeof selector === 'object' && selector.nodeType === 1) { 476 | return elem === selector; 477 | } 478 | return false; 479 | } 480 | 481 | function getCurrentFocusedElement() { 482 | var activeElement = document.activeElement; 483 | if (activeElement && activeElement !== document.body) { 484 | return activeElement; 485 | } 486 | } 487 | 488 | function extend(out) { 489 | out = out || {}; 490 | for (var i = 1; i < arguments.length; i++) { 491 | if (!arguments[i]) { 492 | continue; 493 | } 494 | for (var key in arguments[i]) { 495 | if (arguments[i].hasOwnProperty(key) && 496 | arguments[i][key] !== undefined) { 497 | out[key] = arguments[i][key]; 498 | } 499 | } 500 | } 501 | return out; 502 | } 503 | 504 | function exclude(elemList, excludedElem) { 505 | if (!Array.isArray(excludedElem)) { 506 | excludedElem = [excludedElem]; 507 | } 508 | for (var i = 0, index; i < excludedElem.length; i++) { 509 | index = elemList.indexOf(excludedElem[i]); 510 | if (index >= 0) { 511 | elemList.splice(index, 1); 512 | } 513 | } 514 | return elemList; 515 | } 516 | 517 | function isNavigable(elem, sectionId, verifySectionSelector) { 518 | if (! elem || !sectionId || 519 | !_sections[sectionId] || _sections[sectionId].disabled) { 520 | return false; 521 | } 522 | if ((elem.offsetWidth <= 0 && elem.offsetHeight <= 0) || 523 | elem.hasAttribute('disabled')) { 524 | return false; 525 | } 526 | if (verifySectionSelector && 527 | !matchSelector(elem, _sections[sectionId].selector)) { 528 | return false; 529 | } 530 | if (typeof _sections[sectionId].navigableFilter === 'function') { 531 | if (_sections[sectionId].navigableFilter(elem, sectionId) === false) { 532 | return false; 533 | } 534 | } else if (typeof GlobalConfig.navigableFilter === 'function') { 535 | if (GlobalConfig.navigableFilter(elem, sectionId) === false) { 536 | return false; 537 | } 538 | } 539 | return true; 540 | } 541 | 542 | function getSectionId(elem) { 543 | for (var id in _sections) { 544 | if (!_sections[id].disabled && 545 | matchSelector(elem, _sections[id].selector)) { 546 | return id; 547 | } 548 | } 549 | } 550 | 551 | function getSectionNavigableElements(sectionId) { 552 | return parseSelector(_sections[sectionId].selector).filter(function(elem) { 553 | return isNavigable(elem, sectionId); 554 | }); 555 | } 556 | 557 | function getSectionDefaultElement(sectionId) { 558 | var defaultElement = _sections[sectionId].defaultElement; 559 | if (!defaultElement) { 560 | return null; 561 | } 562 | if (typeof defaultElement === 'string') { 563 | // defaultElement = parseSelector(defaultElement)[0]; // bug ! 564 | var elements = parseSelector(defaultElement); 565 | // check each element to see if it's navigable and stop when one has been found 566 | for (var element of elements) { 567 | if (isNavigable(element, sectionId, true)) { 568 | return element; 569 | } 570 | } 571 | return null; 572 | } else if ($ && defaultElement instanceof $) { 573 | defaultElement = defaultElement.get(0); 574 | if (isNavigable(defaultElement, sectionId, true)) { 575 | return defaultElement; 576 | } 577 | } 578 | return null; 579 | } 580 | 581 | function getSectionLastFocusedElement(sectionId) { 582 | var lastFocusedElement = _sections[sectionId].lastFocusedElement; 583 | if (!isNavigable(lastFocusedElement, sectionId, true)) { 584 | return null; 585 | } 586 | return lastFocusedElement; 587 | } 588 | 589 | function fireEvent(elem, type, details, cancelable) { 590 | if (arguments.length < 4) { 591 | cancelable = true; 592 | } 593 | var evt = document.createEvent('CustomEvent'); 594 | evt.initCustomEvent(EVENT_PREFIX + type, true, cancelable, details); 595 | return elem.dispatchEvent(evt); 596 | } 597 | 598 | function focusElement(elem, sectionId, direction) { 599 | if (!elem) { 600 | return false; 601 | } 602 | 603 | var currentFocusedElement = getCurrentFocusedElement(); 604 | 605 | var focusNscroll = function () { 606 | if (_sections[sectionId].scrollOptions !== undefined && _sections[sectionId].scrollOptions !== "") { 607 | elem.focus({ preventScroll: true }); 608 | elem.scrollIntoView(_sections[sectionId].scrollOptions); 609 | } else if (GlobalConfig.scrollOptions !== undefined && GlobalConfig.scrollOptions !== "") { 610 | elem.focus({ preventScroll: true }); 611 | elem.scrollIntoView(GlobalConfig.scrollOptions); 612 | } else { 613 | elem.focus(); 614 | } 615 | }; 616 | 617 | var silentFocus = function() { 618 | if (currentFocusedElement) { 619 | currentFocusedElement.blur(); 620 | } 621 | 622 | focusNscroll(elem); 623 | focusChanged(elem, sectionId); 624 | }; 625 | 626 | if (_duringFocusChange) { 627 | silentFocus(); 628 | return true; 629 | } 630 | 631 | _duringFocusChange = true; 632 | 633 | if (_pause) { 634 | silentFocus(); 635 | _duringFocusChange = false; 636 | return true; 637 | } 638 | 639 | if (currentFocusedElement) { 640 | var unfocusProperties = { 641 | nextElement: elem, 642 | nextSectionId: sectionId, 643 | direction: direction, 644 | native: false 645 | }; 646 | if (!fireEvent(currentFocusedElement, 'willunfocus', unfocusProperties)) { 647 | _duringFocusChange = false; 648 | return false; 649 | } 650 | currentFocusedElement.blur(); 651 | fireEvent(currentFocusedElement, 'unfocused', unfocusProperties, false); 652 | } 653 | 654 | var focusProperties = { 655 | previousElement: currentFocusedElement, 656 | sectionId: sectionId, 657 | direction: direction, 658 | native: false 659 | }; 660 | if (!fireEvent(elem, 'willfocus', focusProperties)) { 661 | _duringFocusChange = false; 662 | return false; 663 | } 664 | focusNscroll(elem); 665 | fireEvent(elem, 'focused', focusProperties, false); 666 | 667 | _duringFocusChange = false; 668 | 669 | focusChanged(elem, sectionId); 670 | return true; 671 | } 672 | 673 | function focusChanged(elem, sectionId) { 674 | if (!sectionId) { 675 | sectionId = getSectionId(elem); 676 | } 677 | if (sectionId) { 678 | _sections[sectionId].lastFocusedElement = elem; 679 | _lastSectionId = sectionId; 680 | } 681 | } 682 | 683 | function focusExtendedSelector(selector, direction) { 684 | if (selector.charAt(0) == '@') { 685 | if (selector.length == 1) { 686 | return focusSection(); 687 | } else { 688 | var sectionId = selector.substr(1); 689 | return focusSection(sectionId); 690 | } 691 | } else { 692 | var next = parseSelector(selector)[0]; 693 | if (next) { 694 | var nextSectionId = getSectionId(next); 695 | if (isNavigable(next, nextSectionId)) { 696 | return focusElement(next, nextSectionId, direction); 697 | } 698 | } 699 | } 700 | return false; 701 | } 702 | 703 | function focusSection(sectionId) { 704 | var range = []; 705 | var addRange = function(id) { 706 | if (id && range.indexOf(id) < 0 && 707 | _sections[id] && !_sections[id].disabled) { 708 | range.push(id); 709 | } 710 | }; 711 | 712 | if (sectionId) { 713 | addRange(sectionId); 714 | } else { 715 | addRange(_defaultSectionId); 716 | addRange(_lastSectionId); 717 | Object.keys(_sections).map(addRange); 718 | } 719 | 720 | for (var i = 0; i < range.length; i++) { 721 | var id = range[i]; 722 | var next; 723 | 724 | if (_sections[id].enterTo == 'last-focused') { 725 | next = getSectionLastFocusedElement(id) || 726 | getSectionDefaultElement(id) || 727 | getSectionNavigableElements(id)[0]; 728 | } else { 729 | next = getSectionDefaultElement(id) || 730 | getSectionLastFocusedElement(id) || 731 | getSectionNavigableElements(id)[0]; 732 | } 733 | 734 | if (next) { 735 | return focusElement(next, id); 736 | } 737 | } 738 | 739 | return false; 740 | } 741 | 742 | function fireNavigatefailed(elem, direction) { 743 | fireEvent(elem, 'navigatefailed', { 744 | direction: direction 745 | }, false); 746 | } 747 | 748 | function gotoLeaveFor(sectionId, direction) { 749 | if (_sections[sectionId].leaveFor && 750 | _sections[sectionId].leaveFor[direction] !== undefined) { 751 | var next = _sections[sectionId].leaveFor[direction]; 752 | 753 | if (typeof next === 'string') { 754 | if (next === '') { 755 | return null; 756 | } 757 | return focusExtendedSelector(next, direction); 758 | } 759 | 760 | if ($ && next instanceof $) { 761 | next = next.get(0); 762 | } 763 | 764 | var nextSectionId = getSectionId(next); 765 | if (isNavigable(next, nextSectionId)) { 766 | return focusElement(next, nextSectionId, direction); 767 | } 768 | } 769 | return false; 770 | } 771 | 772 | function focusNext(direction, currentFocusedElement, currentSectionId) { 773 | var extSelector = 774 | currentFocusedElement.getAttribute('data-sn-' + direction); 775 | if (typeof extSelector === 'string') { 776 | if (extSelector === '' || 777 | !focusExtendedSelector(extSelector, direction)) { 778 | fireNavigatefailed(currentFocusedElement, direction); 779 | return false; 780 | } 781 | return true; 782 | } 783 | 784 | var sectionNavigableElements = {}; 785 | var allNavigableElements = []; 786 | for (var id in _sections) { 787 | sectionNavigableElements[id] = getSectionNavigableElements(id); 788 | allNavigableElements = 789 | allNavigableElements.concat(sectionNavigableElements[id]); 790 | } 791 | 792 | var config = extend({}, GlobalConfig, _sections[currentSectionId]); 793 | var next; 794 | 795 | if (config.restrict == 'self-only' || config.restrict == 'self-first') { 796 | var currentSectionNavigableElements = 797 | sectionNavigableElements[currentSectionId]; 798 | 799 | next = navigate( 800 | currentFocusedElement, 801 | direction, 802 | exclude(currentSectionNavigableElements, currentFocusedElement), 803 | config 804 | ); 805 | 806 | if (!next && config.restrict == 'self-first') { 807 | next = navigate( 808 | currentFocusedElement, 809 | direction, 810 | exclude(allNavigableElements, currentSectionNavigableElements), 811 | config 812 | ); 813 | } 814 | } else { 815 | next = navigate( 816 | currentFocusedElement, 817 | direction, 818 | exclude(allNavigableElements, currentFocusedElement), 819 | config 820 | ); 821 | } 822 | 823 | if (next) { 824 | _sections[currentSectionId].previous = { 825 | target: currentFocusedElement, 826 | destination: next, 827 | reverse: REVERSE[direction] 828 | }; 829 | 830 | var nextSectionId = getSectionId(next); 831 | 832 | if (currentSectionId != nextSectionId) { 833 | var result = gotoLeaveFor(currentSectionId, direction); 834 | if (result) { 835 | return true; 836 | } else if (result === null) { 837 | fireNavigatefailed(currentFocusedElement, direction); 838 | return false; 839 | } 840 | 841 | var enterToElement; 842 | switch (_sections[nextSectionId].enterTo) { 843 | case 'last-focused': 844 | enterToElement = getSectionLastFocusedElement(nextSectionId) || 845 | getSectionDefaultElement(nextSectionId); 846 | break; 847 | case 'default-element': 848 | enterToElement = getSectionDefaultElement(nextSectionId); 849 | break; 850 | } 851 | if (enterToElement) { 852 | next = enterToElement; 853 | } 854 | } 855 | 856 | return focusElement(next, nextSectionId, direction); 857 | } else if (gotoLeaveFor(currentSectionId, direction)) { 858 | return true; 859 | } 860 | 861 | fireNavigatefailed(currentFocusedElement, direction); 862 | return false; 863 | } 864 | 865 | function onKeyDown(evt) { 866 | if (!_sectionCount || _pause || 867 | evt.altKey || evt.ctrlKey || evt.metaKey || evt.shiftKey) { 868 | return; 869 | } 870 | 871 | var currentFocusedElement; 872 | var preventDefault = function() { 873 | evt.preventDefault(); 874 | evt.stopPropagation(); 875 | return false; 876 | }; 877 | 878 | var direction = KEYMAPPING[evt.keyCode]; 879 | if (!direction) { 880 | if (evt.keyCode == 13) { 881 | currentFocusedElement = getCurrentFocusedElement(); 882 | if (currentFocusedElement && getSectionId(currentFocusedElement)) { 883 | if (!fireEvent(currentFocusedElement, 'enter-down')) { 884 | return preventDefault(); 885 | } 886 | } 887 | } 888 | return; 889 | } 890 | 891 | currentFocusedElement = getCurrentFocusedElement(); 892 | 893 | if (!currentFocusedElement) { 894 | if (_lastSectionId) { 895 | currentFocusedElement = getSectionLastFocusedElement(_lastSectionId); 896 | } 897 | if (!currentFocusedElement) { 898 | focusSection(); 899 | return preventDefault(); 900 | } 901 | } 902 | 903 | var currentSectionId = getSectionId(currentFocusedElement); 904 | if (!currentSectionId) { 905 | return; 906 | } 907 | 908 | var willmoveProperties = { 909 | direction: direction, 910 | sectionId: currentSectionId, 911 | cause: 'keydown' 912 | }; 913 | 914 | if (fireEvent(currentFocusedElement, 'willmove', willmoveProperties)) { 915 | focusNext(direction, currentFocusedElement, currentSectionId); 916 | } 917 | 918 | return preventDefault(); 919 | } 920 | 921 | function onKeyUp(evt) { 922 | if (evt.altKey || evt.ctrlKey || evt.metaKey || evt.shiftKey) { 923 | return 924 | } 925 | if (!_pause && _sectionCount && evt.keyCode == 13) { 926 | var currentFocusedElement = getCurrentFocusedElement(); 927 | if (currentFocusedElement && getSectionId(currentFocusedElement)) { 928 | if (!fireEvent(currentFocusedElement, 'enter-up')) { 929 | evt.preventDefault(); 930 | evt.stopPropagation(); 931 | } 932 | } 933 | } 934 | } 935 | 936 | function onFocus(evt) { 937 | var target = evt.target; 938 | if (target !== window && target !== document && 939 | _sectionCount && !_duringFocusChange) { 940 | var sectionId = getSectionId(target); 941 | if (sectionId) { 942 | if (_pause) { 943 | focusChanged(target, sectionId); 944 | return; 945 | } 946 | 947 | var focusProperties = { 948 | sectionId: sectionId, 949 | native: true 950 | }; 951 | 952 | if (!fireEvent(target, 'willfocus', focusProperties)) { 953 | _duringFocusChange = true; 954 | target.blur(); 955 | _duringFocusChange = false; 956 | } else { 957 | fireEvent(target, 'focused', focusProperties, false); 958 | focusChanged(target, sectionId); 959 | } 960 | } 961 | } 962 | } 963 | 964 | function onBlur(evt) { 965 | var target = evt.target; 966 | if (target !== window && target !== document && !_pause && 967 | _sectionCount && !_duringFocusChange && getSectionId(target)) { 968 | var unfocusProperties = { 969 | native: true 970 | }; 971 | if (!fireEvent(target, 'willunfocus', unfocusProperties)) { 972 | _duringFocusChange = true; 973 | setTimeout(function() { 974 | target.focus(); 975 | _duringFocusChange = false; 976 | }); 977 | } else { 978 | fireEvent(target, 'unfocused', unfocusProperties, false); 979 | } 980 | } 981 | } 982 | 983 | function onBodyClick(){ 984 | if (document.activeElement === document.body && _lastSectionId 985 | && _sections[_lastSectionId].lastFocusedElement) { 986 | focusElement(_sections[_lastSectionId].lastFocusedElement, _lastSectionId); 987 | } 988 | } 989 | 990 | /*******************/ 991 | /* Public Function */ 992 | /*******************/ 993 | var SpatialNavigation = { 994 | init: function() { 995 | if (!_ready) { 996 | window.addEventListener('keydown', onKeyDown); 997 | window.addEventListener('keyup', onKeyUp); 998 | window.addEventListener('focus', onFocus, true); 999 | window.addEventListener('blur', onBlur, true); 1000 | document.body.addEventListener("click", onBodyClick); 1001 | _ready = true; 1002 | } 1003 | }, 1004 | 1005 | uninit: function() { 1006 | window.removeEventListener('blur', onBlur, true); 1007 | window.removeEventListener('focus', onFocus, true); 1008 | window.removeEventListener('keyup', onKeyUp); 1009 | window.removeEventListener('keydown', onKeyDown); 1010 | document.body.removeEventListener("click", onBodyClick); 1011 | SpatialNavigation.clear(); 1012 | _idPool = 0; 1013 | _ready = false; 1014 | }, 1015 | 1016 | clear: function() { 1017 | _sections = {}; 1018 | _sectionCount = 0; 1019 | _defaultSectionId = ''; 1020 | _lastSectionId = ''; 1021 | _duringFocusChange = false; 1022 | }, 1023 | 1024 | reset: function(sectionId) { 1025 | if (sectionId) { 1026 | _sections[sectionId].lastFocusedElement = null; 1027 | _sections[sectionId].previous = null; 1028 | } else { 1029 | for (const id in _sections) { 1030 | const section = _sections[id]; 1031 | section.lastFocusedElement = null; 1032 | section.previous = null; 1033 | } 1034 | } 1035 | }, 1036 | 1037 | // set(); 1038 | // set(, ); 1039 | set: function() { 1040 | var sectionId, config; 1041 | 1042 | if (typeof arguments[0] === 'object') { 1043 | config = arguments[0]; 1044 | } else if (typeof arguments[0] === 'string' && 1045 | typeof arguments[1] === 'object') { 1046 | sectionId = arguments[0]; 1047 | config = arguments[1]; 1048 | if (!_sections[sectionId]) { 1049 | throw new Error('Section "' + sectionId + '" doesn\'t exist!'); 1050 | } 1051 | } else { 1052 | return; 1053 | } 1054 | 1055 | for (var key in config) { 1056 | if (GlobalConfig[key] !== undefined) { 1057 | if (sectionId) { 1058 | _sections[sectionId][key] = config[key]; 1059 | } else if (config[key] !== undefined) { 1060 | GlobalConfig[key] = config[key]; 1061 | } 1062 | } 1063 | } 1064 | 1065 | if (sectionId) { 1066 | // remove "undefined" items 1067 | _sections[sectionId] = extend({}, _sections[sectionId]); 1068 | } 1069 | }, 1070 | 1071 | // add(); 1072 | // add(, ); 1073 | add: function() { 1074 | var sectionId; 1075 | var config = {}; 1076 | 1077 | if (typeof arguments[0] === 'object') { 1078 | config = arguments[0]; 1079 | } else if (typeof arguments[0] === 'string' && 1080 | typeof arguments[1] === 'object') { 1081 | sectionId = arguments[0]; 1082 | config = arguments[1]; 1083 | } 1084 | 1085 | if (!sectionId) { 1086 | sectionId = (typeof config.id === 'string') ? config.id : generateId(); 1087 | } 1088 | 1089 | if (_sections[sectionId]) { 1090 | throw new Error('Section "' + sectionId + '" has already existed!'); 1091 | } 1092 | 1093 | _sections[sectionId] = {}; 1094 | _sectionCount++; 1095 | 1096 | SpatialNavigation.set(sectionId, config); 1097 | 1098 | return sectionId; 1099 | }, 1100 | 1101 | remove: function(sectionId) { 1102 | if (!sectionId || typeof sectionId !== 'string') { 1103 | throw new Error('Please assign the "sectionId"!'); 1104 | } 1105 | if (_sections[sectionId]) { 1106 | _sections[sectionId] = undefined; 1107 | _sections = extend({}, _sections); 1108 | _sectionCount--; 1109 | if (_lastSectionId === sectionId) { 1110 | _lastSectionId = ''; 1111 | } 1112 | return true; 1113 | } 1114 | return false; 1115 | }, 1116 | 1117 | disable: function(sectionId) { 1118 | if (_sections[sectionId]) { 1119 | _sections[sectionId].disabled = true; 1120 | return true; 1121 | } 1122 | return false; 1123 | }, 1124 | 1125 | enable: function(sectionId) { 1126 | if (_sections[sectionId]) { 1127 | _sections[sectionId].disabled = false; 1128 | return true; 1129 | } 1130 | return false; 1131 | }, 1132 | 1133 | pause: function() { 1134 | _pause = true; 1135 | }, 1136 | 1137 | resume: function() { 1138 | _pause = false; 1139 | }, 1140 | 1141 | // focus([silent]) 1142 | // focus(, [silent]) 1143 | // focus(, [silent]) 1144 | // Note: "silent" is optional and default to false 1145 | focus: function(elem, silent) { 1146 | var result = false; 1147 | 1148 | if (silent === undefined && typeof elem === 'boolean') { 1149 | silent = elem; 1150 | elem = undefined; 1151 | } 1152 | 1153 | var autoPause = !_pause && silent; 1154 | 1155 | if (autoPause) { 1156 | SpatialNavigation.pause(); 1157 | } 1158 | 1159 | if (!elem) { 1160 | result = focusSection(); 1161 | } else { 1162 | if (typeof elem === 'string') { 1163 | if (_sections[elem]) { 1164 | result = focusSection(elem); 1165 | } else { 1166 | result = focusExtendedSelector(elem); 1167 | } 1168 | } else { 1169 | if ($ && elem instanceof $) { 1170 | elem = elem.get(0); 1171 | } 1172 | 1173 | var nextSectionId = getSectionId(elem); 1174 | if (isNavigable(elem, nextSectionId)) { 1175 | result = focusElement(elem, nextSectionId); 1176 | } 1177 | } 1178 | } 1179 | 1180 | if (autoPause) { 1181 | SpatialNavigation.resume(); 1182 | } 1183 | 1184 | return result; 1185 | }, 1186 | 1187 | // move() 1188 | // move(, ) 1189 | move: function(direction, selector) { 1190 | direction = direction.toLowerCase(); 1191 | if (!REVERSE[direction]) { 1192 | return false; 1193 | } 1194 | 1195 | var elem = selector ? 1196 | parseSelector(selector)[0] : getCurrentFocusedElement(); 1197 | if (!elem) { 1198 | return false; 1199 | } 1200 | 1201 | var sectionId = getSectionId(elem); 1202 | if (!sectionId) { 1203 | return false; 1204 | } 1205 | 1206 | var willmoveProperties = { 1207 | direction: direction, 1208 | sectionId: sectionId, 1209 | cause: 'api' 1210 | }; 1211 | 1212 | if (!fireEvent(elem, 'willmove', willmoveProperties)) { 1213 | return false; 1214 | } 1215 | 1216 | return focusNext(direction, elem, sectionId); 1217 | }, 1218 | 1219 | // makeFocusable() 1220 | // makeFocusable() 1221 | makeFocusable: function(sectionId) { 1222 | var doMakeFocusable = function(section) { 1223 | var tabIndexIgnoreList = section.tabIndexIgnoreList !== undefined ? 1224 | section.tabIndexIgnoreList : GlobalConfig.tabIndexIgnoreList; 1225 | parseSelector(section.selector).forEach(function(elem) { 1226 | if (!matchSelector(elem, tabIndexIgnoreList)) { 1227 | if (!elem.getAttribute('tabindex')) { 1228 | elem.setAttribute('tabindex', '-1'); 1229 | } 1230 | } 1231 | }); 1232 | }; 1233 | 1234 | if (sectionId) { 1235 | if (_sections[sectionId]) { 1236 | doMakeFocusable(_sections[sectionId]); 1237 | } else { 1238 | throw new Error('Section "' + sectionId + '" doesn\'t exist!'); 1239 | } 1240 | } else { 1241 | for (var id in _sections) { 1242 | doMakeFocusable(_sections[id]); 1243 | } 1244 | } 1245 | }, 1246 | 1247 | setDefaultSection: function(sectionId) { 1248 | if (!sectionId) { 1249 | _defaultSectionId = ''; 1250 | } else if (!_sections[sectionId]) { 1251 | throw new Error('Section "' + sectionId + '" doesn\'t exist!'); 1252 | } else { 1253 | _defaultSectionId = sectionId; 1254 | } 1255 | } 1256 | }; 1257 | 1258 | window.SpatialNavigation = SpatialNavigation; 1259 | 1260 | /**********************/ 1261 | /* CommonJS Interface */ 1262 | /**********************/ 1263 | if (typeof module === 'object') { 1264 | module.exports = SpatialNavigation; 1265 | } 1266 | 1267 | /********************/ 1268 | /* jQuery Interface */ 1269 | /********************/ 1270 | if ($) { 1271 | $.SpatialNavigation = function() { 1272 | SpatialNavigation.init(); 1273 | 1274 | if (arguments.length > 0) { 1275 | if ($.isPlainObject(arguments[0])) { 1276 | return SpatialNavigation.add(arguments[0]); 1277 | } else if ($.type(arguments[0]) === 'string' && 1278 | $.isFunction(SpatialNavigation[arguments[0]])) { 1279 | return SpatialNavigation[arguments[0]] 1280 | .apply(SpatialNavigation, [].slice.call(arguments, 1)); 1281 | } 1282 | } 1283 | 1284 | return $.extend({}, SpatialNavigation); 1285 | }; 1286 | 1287 | $.fn.SpatialNavigation = function() { 1288 | var config; 1289 | 1290 | if ($.isPlainObject(arguments[0])) { 1291 | config = arguments[0]; 1292 | } else { 1293 | config = { 1294 | id: arguments[0] 1295 | }; 1296 | } 1297 | 1298 | config.selector = this; 1299 | 1300 | SpatialNavigation.init(); 1301 | if (config.id) { 1302 | SpatialNavigation.remove(config.id); 1303 | } 1304 | SpatialNavigation.add(config); 1305 | SpatialNavigation.makeFocusable(config.id); 1306 | 1307 | return this; 1308 | }; 1309 | } 1310 | })(window.jQuery); 1311 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-js-spatial-navigation", 3 | "version": "2.0.14", 4 | "description": "Vue directive of js-spatial-navigation", 5 | "main": "index.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/spacerefugee/vue-js-spatial-navigation.git" 15 | }, 16 | "keywords": [ 17 | "vue", 18 | "spatial-navigation", 19 | "smart-tv", 20 | "key-navigation" 21 | ], 22 | "author": "Brandon.gh.yang", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/spacerefugee/vue-js-spatial-navigation/issues" 26 | }, 27 | "homepage": "https://github.com/spacerefugee/vue-js-spatial-navigation#readme", 28 | "dependencies": { 29 | "focus-options-polyfill": "^1.6.0", 30 | "scroll-behavior-polyfill": "^2.0.13" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | focus-options-polyfill@^1.6.0: 6 | version "1.6.0" 7 | resolved "https://registry.yarnpkg.com/focus-options-polyfill/-/focus-options-polyfill-1.6.0.tgz#12ef3081dae9801d32dfcffd7b69259ca8f57198" 8 | integrity sha512-uyrAmLZrPnUItQY5wTdg31TO9GGZRGsh/jmohUg9oLmLi/sw5y7LlTV/mwyd6rvbxIOGwmRiv6LcTS8w7Bk9NQ== 9 | 10 | scroll-behavior-polyfill@^2.0.13: 11 | version "2.0.13" 12 | resolved "https://registry.yarnpkg.com/scroll-behavior-polyfill/-/scroll-behavior-polyfill-2.0.13.tgz#f6f4db9eecdb94d5744b85b653440602fcf3b24b" 13 | integrity sha512-X1AC6+k0++Y3XOT8Likr5Dh4XMmwvbTolr3Mp2g1jYPpOQ3++wff/JPfPEK6zqPCkZ88Ac0/OMRW/Uwh16U+HQ== 14 | --------------------------------------------------------------------------------