diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc794c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,117 @@ + +# Created by https://www.gitignore.io/api/macos,windows,linux,node + +### macOS ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon +# Thumbnails +._* +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + + +### Node ### +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + + +# End of https://www.gitignore.io/api/macos,windows,linux,node diff --git a/README.md b/README.md index c561b38..09958f9 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,25 @@ Although SpatialNavigation is a standalone (pure-javascript-based) library, it c + [Demonstrations](https://luke-chang.github.io/js-spatial-navigation/demo/) +Deployment +---------- + +After changing the source file you can create a new build by entering following line: + +```shell +grunt +``` + +This will create a new minified version of the library and saves it into the ``dist`` folder. +In case you haven't Grunt available yet, just enter + +```shell +npm install +``` + +and Grunt as well as all the needed plugins will be installed in your repository folder. Afterwards you +can perform the instruction from above to create a new minified version of the library. + Documentation ------------- @@ -194,7 +213,8 @@ Following is an example with default values. leaveFor: null, restrict: 'self-first', tabIndexIgnoreList: 'a, input, select, textarea, button, iframe, [contentEditable=true]', - navigableFilter: null + navigableFilter: null, + ignoreOffsetDimensionValidation: false } ``` @@ -293,6 +313,15 @@ A callback function that accepts a DOM element as the first argument. SpatialNavigation calls this function every time when it tries to traverse every single candidate. You can ignore arbitrary elements by returning `false`. +#### `ignoreOffsetDimensionValidation` + + + Type: `'boolean'` + + Default: `false` + +When the library checks whether an element is navigable or not, it validates the offset width and height values to be greater than zero +and only then the element is navigable. There're also cases where the offset width and height are zero but the element is still visible and therefore needs to be focusable. +In such cases it's possible to disable the validation at all for a particular section or globally. + ### Custom Attributes SpatialNavigation supports HTML `data-*` attributes as follows: diff --git a/dist/spatial_navigation.min.js b/dist/spatial_navigation.min.js new file mode 100644 index 0000000..00d407b --- /dev/null +++ b/dist/spatial_navigation.min.js @@ -0,0 +1,9 @@ +/** + * A javascript-based implementation of Spatial Navigation. + * + * Copyright (c) 2017 Luke Chang. + * https://github.com/luke-chang/js-spatial-navigation + * + * @license Licensed under the MPL 2.0. + */ +!function(a){"use strict";function b(a){var b=a.getBoundingClientRect(),c={left:b.left,top:b.top,right:b.right,bottom:b.bottom,width:b.width,height:b.height};return c.element=a,c.center={x:c.left+Math.floor(c.width/2),y:c.top+Math.floor(c.height/2)},c.center.left=c.center.right=c.center.x,c.center.top=c.center.bottom=c.center.y,c}function c(a,b,c){for(var d=[[],[],[],[],[],[],[],[],[]],e=0;e=b.left+b.width*k&&(0===h?d[1].push(i):6===h&&d[7].push(i)),i.top<=b.bottom-b.height*k&&(6===h?d[3].push(i):8===h&&d[5].push(i)),i.bottom>=b.top+b.height*k&&(0===h?d[3].push(i):2===h&&d[5].push(i))}}return d}function d(a){return{nearPlumbLineIsBetter:function(b){var c;return c=b.center.x=0:"object"==typeof c&&1===c.nodeType&&b===c}function j(){var a=document.activeElement;if(a&&a!==document.body)return a}function k(a){a=a||{};for(var b=1;b=0&&a.splice(c,1);return a}function m(a,b,c){if(!a||!b||!L[b]||L[b].disabled)return!1;var d="boolean"==typeof L[b].ignoreOffsetDimensionValidation&&L[b].ignoreOffsetDimensionValidation===!0||"boolean"==typeof D.ignoreOffsetDimensionValidation&&D.ignoreOffsetDimensionValidation===!0;if(!d&&a.offsetWidth<=0&&a.offsetHeight<=0||a.hasAttribute("disabled"))return!1;if(c&&!i(a,L[b].selector))return!1;if("function"==typeof L[b].navigableFilter){if(L[b].navigableFilter(a,b)===!1)return!1}else if("function"==typeof D.navigableFilter&&D.navigableFilter(a,b)===!1)return!1;return!0}function n(a){for(var b in L)if(!L[b].disabled&&i(a,L[b].selector))return b}function o(a){return h(L[a].selector).filter(function(b){return m(b,a)})}function p(b){var c=L[b].defaultElement;return c?("string"==typeof c?c=h(c)[0]:a&&c instanceof a&&(c=c.get(0)),m(c,b,!0)?c:null):null}function q(a){var b=L[a].lastFocusedElement;return m(b,a,!0)?b:null}function r(a,b,c,d){arguments.length<4&&(d=!0);var e=document.createEvent("CustomEvent");return e.initCustomEvent(G+b,!0,d,c),a.dispatchEvent(e)}function s(a,b,c){if(!a)return!1;var d=j(),e=function(){d&&d.blur(),a.focus(),t(a,b)};if(P)return e(),!0;if(P=!0,K)return e(),P=!1,!0;if(d){var f={nextElement:a,nextSectionId:b,direction:c,native:!1};if(!r(d,"willunfocus",f))return P=!1,!1;d.blur(),r(d,"unfocused",f,!1)}var g={previousElement:d,sectionId:b,direction:c,native:!1};return r(a,"willfocus",g)?(a.focus(),r(a,"focused",g,!1),P=!1,t(a,b),!0):(P=!1,!1)}function t(a,b){b||(b=n(a)),b&&(L[b].lastFocusedElement=a,O=b)}function u(a,b){if("@"==a.charAt(0)){if(1==a.length)return v();var c=a.substr(1);return v(c)}var d=h(a)[0];if(d){var e=n(d);if(m(d,e))return s(d,e,b)}return!1}function v(a){var b=[],c=function(a){a&&b.indexOf(a)<0&&L[a]&&!L[a].disabled&&b.push(a)};a?c(a):(c(N),c(O),Object.keys(L).map(c));for(var d=0;d=0},R={init:function(){J||(window.addEventListener("keydown",z),window.addEventListener("keyup",A),window.addEventListener("focus",B,!0),window.addEventListener("blur",C,!0),J=!0)},uninit:function(){window.removeEventListener("blur",C,!0),window.removeEventListener("focus",B,!0),window.removeEventListener("keyup",A),window.removeEventListener("keydown",z),R.clear(),I=0,J=!1},clear:function(){L={},M=0,N="",O="",P=!1},set:function(){var a,b;if("object"==typeof arguments[0])b=arguments[0];else{if("string"!=typeof arguments[0]||"object"!=typeof arguments[1])return;if(a=arguments[0],b=arguments[1],!L[a])throw new Error('Section "'+a+"\" doesn't exist!")}for(var c in b)void 0!==D[c]&&(a?L[a][c]=b[c]:void 0!==b[c]&&(D[c]=b[c]));a&&(L[a]=k({},L[a]))},add:function(){var a,b={};if("object"==typeof arguments[0]?b=arguments[0]:"string"==typeof arguments[0]&&"object"==typeof arguments[1]&&(a=arguments[0],b=arguments[1]),a||(a="string"==typeof b.id?b.id:g()),L[a])throw new Error('Section "'+a+'" has already existed!');return L[a]={},M++,R.set(a,b),a},remove:function(a){if(!a||"string"!=typeof a)throw new Error('Please assign the "sectionId"!');return!!L[a]&&(L[a]=void 0,L=k({},L),M--,!0)},disable:function(a){return!!L[a]&&(L[a].disabled=!0,!0)},enable:function(a){return!!L[a]&&(L[a].disabled=!1,!0)},pause:function(){K=!0},resume:function(){K=!1},focus:function(b,c){var d=!1;void 0===c&&"boolean"==typeof b&&(c=b,b=void 0);var e=!K&&c;if(e&&R.pause(),b)if("string"==typeof b)d=L[b]?v(b):u(b);else{a&&b instanceof a&&(b=b.get(0));var f=n(b);m(b,f)&&(d=s(b,f))}else d=v();return e&&R.resume(),d},move:function(a,b){if(a=a.toLowerCase(),!F[a])return!1;var c=b?h(b)[0]:j();if(!c)return!1;var d=n(c);if(!d)return!1;var e={direction:a,sectionId:d,cause:"api"};return!!r(c,"willmove",e)&&y(a,c,d)},makeFocusable:function(a){var b=function(a){var b=void 0!==a.tabIndexIgnoreList?a.tabIndexIgnoreList:D.tabIndexIgnoreList;h(a.selector).forEach(function(a){i(a,b)||a.getAttribute("tabindex")||a.setAttribute("tabindex","-1")})};if(a){if(!L[a])throw new Error('Section "'+a+"\" doesn't exist!");b(L[a])}else for(var c in L)b(L[c])},setDefaultSection:function(a){if(a){if(!L[a])throw new Error('Section "'+a+"\" doesn't exist!");N=a}else N=""}};window.SpatialNavigation=R,a&&(a.SpatialNavigation=function(){if(R.init(),arguments.length>0){if(a.isPlainObject(arguments[0]))return R.add(arguments[0]);if("string"===a.type(arguments[0])&&a.isFunction(R[arguments[0]]))return R[arguments[0]].apply(R,[].slice.call(arguments,1))}return a.extend({},R)},a.fn.SpatialNavigation=function(){var b;return b=a.isPlainObject(arguments[0])?arguments[0]:{id:arguments[0]},b.selector=this,R.init(),b.id&&R.remove(b.id),R.add(b),R.makeFocusable(b.id),this})}(window.jQuery); \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js new file mode 100644 index 0000000..0bb4eab --- /dev/null +++ b/gruntfile.js @@ -0,0 +1,32 @@ +const fs = require('fs'); + +module.exports = function(grunt) +{ + require('jit-grunt')(grunt, { + ngtemplates: 'grunt-angular-templates' + }); + + grunt.initConfig({ + /** + * uglify. + */ + uglify: { + options: { + beautify: false, + preserveComments: 'some', + compress: { + drop_console: true + } + }, + release: { + files: { + 'dist/spatial_navigation.min.js': ['src/spatial_navigation.js'] + } + } + } + }); + + grunt.registerTask('default', [ + 'uglify' + ]); +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..317c7e3 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "js-spatial-navigation", + "version": "1.0.0", + "description": "A javascript-based implementation of Spatial Navigation.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/exozet/js-spatial-navigation.git" + }, + "keywords": [ + "spatial", + "navigation", + "javascript" + ], + "author": "Luke Chang", + "license": "MPL-2.0", + "bugs": { + "url": "https://github.com/exozet/js-spatial-navigation/issues" + }, + "homepage": "https://github.com/exozet/js-spatial-navigation#readme", + "devDependencies": { + "grunt": "^1.0.1", + "grunt-contrib-uglify": "^2.0.0", + "jit-grunt": "^0.10.0" + } +} diff --git a/spatial_navigation.js b/src/spatial_navigation.js similarity index 96% rename from spatial_navigation.js rename to src/spatial_navigation.js index d2a6834..d70899d 100644 --- a/spatial_navigation.js +++ b/src/spatial_navigation.js @@ -1,10 +1,10 @@ -/* +/** * A javascript-based implementation of Spatial Navigation. * * Copyright (c) 2017 Luke Chang. * https://github.com/luke-chang/js-spatial-navigation * - * Licensed under the MPL 2.0. + * @license Licensed under the MPL 2.0. */ ;(function($) { 'use strict'; @@ -32,7 +32,20 @@ restrict: 'self-first', // 'self-first', 'self-only', 'none' tabIndexIgnoreList: 'a, input, select, textarea, button, iframe, [contentEditable=true]', - navigableFilter: null + navigableFilter: null, + + /** + * Disables offset dimension validation. + * + * Usually before an element get focused it'll be checked + * whether it's navigable or not. To do so it checks, among other things, whether + * the offsetWidth or the offsetHeight is greater than null because then it means + * that element is visible. Some elements haven't any offsetWidth or offsetHeight + * defined even though they're visible to the viewer. To make those elements + * navigable this option can be set to true and the validation will be ignored. + * That option is available for a particular section or globally. + */ + ignoreOffsetDimensionValidation: false }; /*********************/ @@ -518,7 +531,11 @@ !_sections[sectionId] || _sections[sectionId].disabled) { return false; } - if ((elem.offsetWidth <= 0 && elem.offsetHeight <= 0) || + + var ignoreOffsetDimensionValidation = (typeof _sections[sectionId].ignoreOffsetDimensionValidation === 'boolean' && _sections[sectionId].ignoreOffsetDimensionValidation === true) || + (typeof GlobalConfig.ignoreOffsetDimensionValidation === 'boolean' && GlobalConfig.ignoreOffsetDimensionValidation === true); + + if ((!ignoreOffsetDimensionValidation && elem.offsetWidth <= 0 && elem.offsetHeight <= 0) || elem.hasAttribute('disabled')) { return false; } diff --git a/test/offset-dimensions-test.html b/test/offset-dimensions-test.html new file mode 100644 index 0000000..eceb8ca --- /dev/null +++ b/test/offset-dimensions-test.html @@ -0,0 +1,84 @@ + + + + + + Spatial Navigation - Offset Dimensions Test + + + + + +
+

Spatial Navigation - Offset Dimensions Test

+ + +
+ + + + + \ No newline at end of file