emulatetab.joelpurra.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. /*global document:true, jQuery:true */
  2. // Set up namespace, if needed
  3. var JoelPurra = JoelPurra || {};
  4. (function(document, $, namespace, pluginName) {
  5. "use strict";
  6. var eventNamespace = "." + pluginName,
  7. // TODO: get code for :focusable, :tabbable from jQuery UI?
  8. focusable = ":input, a[href]",
  9. // Keep a reference to the last focused element, use as a last resort.
  10. lastFocusedElement = null,
  11. // Private methods
  12. internal = {
  13. escapeSelectorName: function(str) {
  14. // Based on http://api.jquery.com/category/selectors/
  15. // Still untested
  16. return str.replace(/(!"#$%&'\(\)\*\+,\.\/:;<=>\?@\[\]^`\{\|\}~)/g, "\\\\$1");
  17. },
  18. findNextFocusable: function($from, offset) {
  19. var $focusable = $(focusable)
  20. .not(":disabled")
  21. .not(":hidden")
  22. .not("a[href]:empty");
  23. if ($from[0].tagName === "INPUT" && $from[0].type === "radio" && $from[0].name !== "") {
  24. var name = internal.escapeSelectorName($from[0].name);
  25. $focusable = $focusable
  26. .not("input[type=radio][name=" + name + "]")
  27. .add($from);
  28. }
  29. var currentIndex = $focusable.index($from);
  30. var nextIndex = (currentIndex + offset) % $focusable.length;
  31. if (nextIndex <= -1) {
  32. nextIndex = $focusable.length + nextIndex;
  33. }
  34. var $next = $focusable.eq(nextIndex);
  35. return $next;
  36. },
  37. focusInElement: function(event) {
  38. lastFocusedElement = event.target;
  39. },
  40. tryGetElementAsNonEmptyJQueryObject: function(selector) {
  41. try {
  42. var $element = $(selector);
  43. if ( !! $element && $element.size() !== 0) {
  44. return $element;
  45. }
  46. } catch (e) {
  47. // Could not use element. Do nothing.
  48. }
  49. return null;
  50. },
  51. // Fix for EmulateTab Issue #2
  52. // https://github.com/joelpurra/emulatetab/issues/2
  53. // Combined function to get the focused element, trying as long as possible.
  54. // Extra work done trying to avoid problems with security features around
  55. // <input type="file" /> in Firefox (tested using 10.0.1).
  56. // http://stackoverflow.com/questions/9301310/focus-returns-no-element-for-input-type-file-in-firefox
  57. // Problem: http://jsfiddle.net/joelpurra/bzsv7/
  58. // Fixed: http://jsfiddle.net/joelpurra/bzsv7/2/
  59. getFocusedElement: function() {
  60. // 1. Try the well-known, recommended method first.
  61. //
  62. // 2. Fall back to a fast method that might fail.
  63. // Known to fail for Firefox (tested using 10.0.1) with
  64. // Permission denied to access property "nodeType".
  65. //
  66. // 3. As a last resort, use the last known focused element.
  67. // Has not been tested enough to be sure it works as expected
  68. // in all browsers and scenarios.
  69. //
  70. // 4. Empty fallback
  71. var $focused = internal.tryGetElementAsNonEmptyJQueryObject(":focus") || internal.tryGetElementAsNonEmptyJQueryObject(document.activeElement) || internal.tryGetElementAsNonEmptyJQueryObject(lastFocusedElement) || $();
  72. return $focused;
  73. },
  74. emulateTabbing: function($from, offset) {
  75. var $next = internal.findNextFocusable($from, offset);
  76. $next.focus();
  77. },
  78. initializeAtLoad: function() {
  79. // Start listener that keep track of the last focused element.
  80. $(document)
  81. .on("focusin" + eventNamespace, internal.focusInElement);
  82. }
  83. },
  84. plugin = {
  85. tab: function($from, offset) {
  86. // Tab from focused element with offset, .tab(-1)
  87. if ($.isNumeric($from)) {
  88. offset = $from;
  89. $from = undefined;
  90. }
  91. $from = $from || plugin.getFocused();
  92. offset = offset || +1;
  93. internal.emulateTabbing($from, offset);
  94. },
  95. forwardTab: function($from) {
  96. return plugin.tab($from, +1);
  97. },
  98. reverseTab: function($from) {
  99. return plugin.tab($from, -1);
  100. },
  101. getFocused: function() {
  102. return internal.getFocusedElement();
  103. }
  104. },
  105. installJQueryExtensions = function() {
  106. $.extend({
  107. emulateTab: function($from, offset) {
  108. return plugin.tab($from, offset);
  109. }
  110. });
  111. $.fn.extend({
  112. emulateTab: function(offset) {
  113. return plugin.tab(this, offset);
  114. }
  115. });
  116. },
  117. init = function() {
  118. namespace[pluginName] = plugin;
  119. installJQueryExtensions();
  120. // EmulateTab initializes listener(s) when jQuery is ready
  121. $(internal.initializeAtLoad);
  122. };
  123. init();
  124. }(document, jQuery, JoelPurra, "EmulateTab"));