focus.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. define("dijit/focus", [
  2. "dojo/aspect",
  3. "dojo/_base/declare", // declare
  4. "dojo/dom", // domAttr.get dom.isDescendant
  5. "dojo/dom-attr", // domAttr.get dom.isDescendant
  6. "dojo/dom-construct", // connect to domConstruct.empty, domConstruct.destroy
  7. "dojo/Evented",
  8. "dojo/_base/lang", // lang.hitch
  9. "dojo/on",
  10. "dojo/domReady",
  11. "dojo/_base/sniff", // has("ie")
  12. "dojo/Stateful",
  13. "dojo/_base/window", // win.body
  14. "dojo/window", // winUtils.get
  15. "./a11y", // a11y.isTabNavigable
  16. "./registry", // registry.byId
  17. "./main" // to set dijit.focus
  18. ], function(aspect, declare, dom, domAttr, domConstruct, Evented, lang, on, domReady, has, Stateful, win, winUtils,
  19. a11y, registry, dijit){
  20. // module:
  21. // dijit/focus
  22. var FocusManager = declare([Stateful, Evented], {
  23. // summary:
  24. // Tracks the currently focused node, and which widgets are currently "active".
  25. // Access via require(["dijit/focus"], function(focus){ ... }).
  26. //
  27. // A widget is considered active if it or a descendant widget has focus,
  28. // or if a non-focusable node of this widget or a descendant was recently clicked.
  29. //
  30. // Call focus.watch("curNode", callback) to track the current focused DOMNode,
  31. // or focus.watch("activeStack", callback) to track the currently focused stack of widgets.
  32. //
  33. // Call focus.on("widget-blur", func) or focus.on("widget-focus", ...) to monitor when
  34. // when widgets become active/inactive
  35. //
  36. // Finally, focus(node) will focus a node, suppressing errors if the node doesn't exist.
  37. // curNode: DomNode
  38. // Currently focused item on screen
  39. curNode: null,
  40. // activeStack: dijit/_WidgetBase[]
  41. // List of currently active widgets (focused widget and it's ancestors)
  42. activeStack: [],
  43. constructor: function(){
  44. // Don't leave curNode/prevNode pointing to bogus elements
  45. var check = lang.hitch(this, function(node){
  46. if(dom.isDescendant(this.curNode, node)){
  47. this.set("curNode", null);
  48. }
  49. if(dom.isDescendant(this.prevNode, node)){
  50. this.set("prevNode", null);
  51. }
  52. });
  53. aspect.before(domConstruct, "empty", check);
  54. aspect.before(domConstruct, "destroy", check);
  55. },
  56. registerIframe: function(/*DomNode*/ iframe){
  57. // summary:
  58. // Registers listeners on the specified iframe so that any click
  59. // or focus event on that iframe (or anything in it) is reported
  60. // as a focus/click event on the `<iframe>` itself.
  61. // description:
  62. // Currently only used by editor.
  63. // returns:
  64. // Handle with remove() method to deregister.
  65. return this.registerWin(iframe.contentWindow, iframe);
  66. },
  67. registerWin: function(/*Window?*/targetWindow, /*DomNode?*/ effectiveNode){
  68. // summary:
  69. // Registers listeners on the specified window (either the main
  70. // window or an iframe's window) to detect when the user has clicked somewhere
  71. // or focused somewhere.
  72. // description:
  73. // Users should call registerIframe() instead of this method.
  74. // targetWindow:
  75. // If specified this is the window associated with the iframe,
  76. // i.e. iframe.contentWindow.
  77. // effectiveNode:
  78. // If specified, report any focus events inside targetWindow as
  79. // an event on effectiveNode, rather than on evt.target.
  80. // returns:
  81. // Handle with remove() method to deregister.
  82. // TODO: make this function private in 2.0; Editor/users should call registerIframe(),
  83. // Listen for blur and focus events on targetWindow's document.
  84. var _this = this,
  85. body = targetWindow.document && targetWindow.document.body;
  86. if(body){
  87. var mdh = on(body, 'mousedown', function(evt){
  88. _this._justMouseDowned = true;
  89. // Use a 13 ms timeout to work-around Chrome resolving too fast and focusout
  90. // events not seeing that a mousedown just happened when a popup closes.
  91. // See https://bugs.dojotoolkit.org/ticket/17668
  92. setTimeout(function(){ _this._justMouseDowned = false; }, 13);
  93. // workaround weird IE bug where the click is on an orphaned node
  94. // (first time clicking a Select/DropDownButton inside a TooltipDialog).
  95. // actually, strangely this is happening on latest chrome too.
  96. if(evt && evt.target && evt.target.parentNode == null){
  97. return;
  98. }
  99. _this._onTouchNode(effectiveNode || evt.target, "mouse");
  100. });
  101. var fih = on(body, 'focusin', function(evt){
  102. // When you refocus the browser window, IE gives an event with an empty srcElement
  103. if(!evt.target.tagName) { return; }
  104. // IE reports that nodes like <body> have gotten focus, even though they have tabIndex=-1,
  105. // ignore those events
  106. var tag = evt.target.tagName.toLowerCase();
  107. if(tag == "#document" || tag == "body"){ return; }
  108. if(a11y.isTabNavigable(evt.target)){
  109. // If condition doesn't seem quite right, but it is correctly preventing focus events for
  110. // clicks on disabled buttons.
  111. _this._onFocusNode(effectiveNode || evt.target);
  112. }else{
  113. // Previous code called _onTouchNode() for any activate event on a non-focusable node. Can
  114. // probably just ignore such an event as it will be handled by onmousedown handler above, but
  115. // leaving the code for now.
  116. _this._onTouchNode(effectiveNode || evt.target);
  117. }
  118. });
  119. var foh = on(body, 'focusout', function(evt){
  120. _this._onBlurNode(effectiveNode || evt.target);
  121. });
  122. return {
  123. remove: function(){
  124. mdh.remove();
  125. fih.remove();
  126. foh.remove();
  127. mdh = fih = foh = null;
  128. body = null; // prevent memory leak (apparent circular reference via closure)
  129. }
  130. };
  131. }
  132. },
  133. _onBlurNode: function(/*DomNode*/ node){
  134. // summary:
  135. // Called when focus leaves a node.
  136. // Usually ignored, _unless_ it *isn't* followed by touching another node,
  137. // which indicates that we tabbed off the last field on the page,
  138. // in which case every widget is marked inactive
  139. // If the blur event isn't followed by a focus event, it means the user clicked on something unfocusable,
  140. // so clear focus.
  141. if(this._clearFocusTimer){
  142. clearTimeout(this._clearFocusTimer);
  143. }
  144. this._clearFocusTimer = setTimeout(lang.hitch(this, function(){
  145. this.set("prevNode", this.curNode);
  146. this.set("curNode", null);
  147. }), 0);
  148. if(this._justMouseDowned){
  149. // the mouse down caused a new widget to be marked as active; this blur event
  150. // is coming late, so ignore it.
  151. return;
  152. }
  153. // If the blur event isn't followed by a focus or touch event then mark all widgets as inactive.
  154. if(this._clearActiveWidgetsTimer){
  155. clearTimeout(this._clearActiveWidgetsTimer);
  156. }
  157. this._clearActiveWidgetsTimer = setTimeout(lang.hitch(this, function(){
  158. delete this._clearActiveWidgetsTimer;
  159. this._setStack([]);
  160. }), 100);
  161. },
  162. _onTouchNode: function(/*DomNode*/ node, /*String*/ by){
  163. // summary:
  164. // Callback when node is focused or mouse-downed
  165. // node:
  166. // The node that was touched.
  167. // by:
  168. // "mouse" if the focus/touch was caused by a mouse down event
  169. // ignore the recent blurNode event
  170. if(this._clearActiveWidgetsTimer){
  171. clearTimeout(this._clearActiveWidgetsTimer);
  172. delete this._clearActiveWidgetsTimer;
  173. }
  174. // compute stack of active widgets (ex: ComboButton --> Menu --> MenuItem)
  175. var newStack=[];
  176. try{
  177. while(node){
  178. var popupParent = domAttr.get(node, "dijitPopupParent");
  179. if(popupParent){
  180. node=registry.byId(popupParent).domNode;
  181. }else if(node.tagName && node.tagName.toLowerCase() == "body"){
  182. // is this the root of the document or just the root of an iframe?
  183. if(node === win.body()){
  184. // node is the root of the main document
  185. break;
  186. }
  187. // otherwise, find the iframe this node refers to (can't access it via parentNode,
  188. // need to do this trick instead). window.frameElement is supported in IE/FF/Webkit
  189. node=winUtils.get(node.ownerDocument).frameElement;
  190. }else{
  191. // if this node is the root node of a widget, then add widget id to stack,
  192. // except ignore clicks on disabled widgets (actually focusing a disabled widget still works,
  193. // to support MenuItem)
  194. var id = node.getAttribute && node.getAttribute("widgetId"),
  195. widget = id && registry.byId(id);
  196. if(widget && !(by == "mouse" && widget.get("disabled"))){
  197. newStack.unshift(id);
  198. }
  199. node=node.parentNode;
  200. }
  201. }
  202. }catch(e){ /* squelch */ }
  203. this._setStack(newStack, by);
  204. },
  205. _onFocusNode: function(/*DomNode*/ node){
  206. // summary:
  207. // Callback when node is focused
  208. if(!node){
  209. return;
  210. }
  211. if(node.nodeType == 9){
  212. // Ignore focus events on the document itself. This is here so that
  213. // (for example) clicking the up/down arrows of a spinner
  214. // (which don't get focus) won't cause that widget to blur. (FF issue)
  215. return;
  216. }
  217. // There was probably a blur event right before this event, but since we have a new focus, don't
  218. // do anything with the blur
  219. if(this._clearFocusTimer){
  220. clearTimeout(this._clearFocusTimer);
  221. delete this._clearFocusTimer;
  222. }
  223. this._onTouchNode(node);
  224. if(node == this.curNode){ return; }
  225. this.set("prevNode", this.curNode);
  226. this.set("curNode", node);
  227. },
  228. _setStack: function(/*String[]*/ newStack, /*String*/ by){
  229. // summary:
  230. // The stack of active widgets has changed. Send out appropriate events and records new stack.
  231. // newStack:
  232. // array of widget id's, starting from the top (outermost) widget
  233. // by:
  234. // "mouse" if the focus/touch was caused by a mouse down event
  235. var oldStack = this.activeStack, lastOldIdx = oldStack.length - 1, lastNewIdx = newStack.length - 1;
  236. if(newStack[lastNewIdx] == oldStack[lastOldIdx]){
  237. // no changes, return now to avoid spurious notifications about changes to activeStack
  238. return;
  239. }
  240. this.set("activeStack", newStack);
  241. var widget, i;
  242. // for all elements that have gone out of focus, set focused=false
  243. for(i = lastOldIdx; i >= 0 && oldStack[i] != newStack[i]; i--){
  244. widget = registry.byId(oldStack[i]);
  245. if(widget){
  246. widget._hasBeenBlurred = true; // TODO: used by form widgets, should be moved there
  247. widget.set("focused", false);
  248. if(widget._focusManager == this){
  249. widget._onBlur(by);
  250. }
  251. this.emit("widget-blur", widget, by);
  252. }
  253. }
  254. // for all element that have come into focus, set focused=true
  255. for(i++; i <= lastNewIdx; i++){
  256. widget = registry.byId(newStack[i]);
  257. if(widget){
  258. widget.set("focused", true);
  259. if(widget._focusManager == this){
  260. widget._onFocus(by);
  261. }
  262. this.emit("widget-focus", widget, by);
  263. }
  264. }
  265. },
  266. focus: function(node){
  267. // summary:
  268. // Focus the specified node, suppressing errors if they occur
  269. if(node){
  270. try{ node.focus(); }catch(e){/*quiet*/}
  271. }
  272. }
  273. });
  274. var singleton = new FocusManager();
  275. // register top window and all the iframes it contains
  276. domReady(function(){
  277. var handle = singleton.registerWin(winUtils.get(document));
  278. if(has("ie")){
  279. on(window, "unload", function(){
  280. if(handle){ // because this gets called twice when doh.robot is running
  281. handle.remove();
  282. handle = null;
  283. }
  284. });
  285. }
  286. });
  287. // Setup dijit.focus as a pointer to the singleton but also (for backwards compatibility)
  288. // as a function to set focus. Remove for 2.0.
  289. dijit.focus = function(node){
  290. singleton.focus(node); // indirection here allows dijit/_base/focus.js to override behavior
  291. };
  292. for(var attr in singleton){
  293. if(!/^_/.test(attr)){
  294. dijit.focus[attr] = typeof singleton[attr] == "function" ? lang.hitch(singleton, attr) : singleton[attr];
  295. }
  296. }
  297. singleton.watch(function(attr, oldVal, newVal){
  298. dijit.focus[attr] = newVal;
  299. });
  300. return singleton;
  301. });