focus.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. /*
  2. Copyright (c) 2004-2022, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dijit._base.focus"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dijit._base.focus"] = true;
  8. dojo.provide("dijit._base.focus");
  9. dojo.require("dojo.window");
  10. dojo.require("dijit._base.manager");
  11. // summary:
  12. // These functions are used to query or set the focus and selection.
  13. //
  14. // Also, they trace when widgets become activated/deactivated,
  15. // so that the widget can fire _onFocus/_onBlur events.
  16. // "Active" here means something similar to "focused", but
  17. // "focus" isn't quite the right word because we keep track of
  18. // a whole stack of "active" widgets. Example: ComboButton --> Menu -->
  19. // MenuItem. The onBlur event for ComboButton doesn't fire due to focusing
  20. // on the Menu or a MenuItem, since they are considered part of the
  21. // ComboButton widget. It only happens when focus is shifted
  22. // somewhere completely different.
  23. dojo.mixin(dijit, {
  24. // _curFocus: DomNode
  25. // Currently focused item on screen
  26. _curFocus: null,
  27. // _prevFocus: DomNode
  28. // Previously focused item on screen
  29. _prevFocus: null,
  30. isCollapsed: function(){
  31. // summary:
  32. // Returns true if there is no text selected
  33. return dijit.getBookmark().isCollapsed;
  34. },
  35. getBookmark: function(){
  36. // summary:
  37. // Retrieves a bookmark that can be used with moveToBookmark to return to the same range
  38. var bm, rg, tg, sel = dojo.doc.selection, cf = dijit._curFocus;
  39. if(dojo.global.getSelection){
  40. //W3C Range API for selections.
  41. sel = dojo.global.getSelection();
  42. if(sel){
  43. if(sel.isCollapsed){
  44. tg = cf? cf.tagName : "";
  45. if(tg){
  46. //Create a fake rangelike item to restore selections.
  47. tg = tg.toLowerCase();
  48. if(tg == "textarea" ||
  49. (tg == "input" && (!cf.type || cf.type.toLowerCase() == "text"))){
  50. sel = {
  51. start: cf.selectionStart,
  52. end: cf.selectionEnd,
  53. node: cf,
  54. pRange: true
  55. };
  56. return {isCollapsed: (sel.end <= sel.start), mark: sel}; //Object.
  57. }
  58. }
  59. bm = {isCollapsed:true};
  60. if(sel.rangeCount){
  61. bm.mark = sel.getRangeAt(0).cloneRange();
  62. }
  63. }else{
  64. rg = sel.getRangeAt(0);
  65. bm = {isCollapsed: false, mark: rg.cloneRange()};
  66. }
  67. }
  68. }else if(sel){
  69. // If the current focus was a input of some sort and no selection, don't bother saving
  70. // a native bookmark. This is because it causes issues with dialog/page selection restore.
  71. // So, we need to create psuedo bookmarks to work with.
  72. tg = cf ? cf.tagName : "";
  73. tg = tg.toLowerCase();
  74. if(cf && tg && (tg == "button" || tg == "textarea" || tg == "input")){
  75. if(sel.type && sel.type.toLowerCase() == "none"){
  76. return {
  77. isCollapsed: true,
  78. mark: null
  79. }
  80. }else{
  81. rg = sel.createRange();
  82. return {
  83. isCollapsed: rg.text && rg.text.length?false:true,
  84. mark: {
  85. range: rg,
  86. pRange: true
  87. }
  88. };
  89. }
  90. }
  91. bm = {};
  92. //'IE' way for selections.
  93. try{
  94. // createRange() throws exception when dojo in iframe
  95. //and nothing selected, see #9632
  96. rg = sel.createRange();
  97. bm.isCollapsed = !(sel.type == 'Text' ? rg.htmlText.length : rg.length);
  98. }catch(e){
  99. bm.isCollapsed = true;
  100. return bm;
  101. }
  102. if(sel.type.toUpperCase() == 'CONTROL'){
  103. if(rg.length){
  104. bm.mark=[];
  105. var i=0,len=rg.length;
  106. while(i<len){
  107. bm.mark.push(rg.item(i++));
  108. }
  109. }else{
  110. bm.isCollapsed = true;
  111. bm.mark = null;
  112. }
  113. }else{
  114. bm.mark = rg.getBookmark();
  115. }
  116. }else{
  117. console.warn("No idea how to store the current selection for this browser!");
  118. }
  119. return bm; // Object
  120. },
  121. moveToBookmark: function(/*Object*/bookmark){
  122. // summary:
  123. // Moves current selection to a bookmark
  124. // bookmark:
  125. // This should be a returned object from dijit.getBookmark()
  126. var _doc = dojo.doc,
  127. mark = bookmark.mark;
  128. if(mark){
  129. if(dojo.global.getSelection){
  130. //W3C Rangi API (FF, WebKit, Opera, etc)
  131. var sel = dojo.global.getSelection();
  132. if(sel && sel.removeAllRanges){
  133. if(mark.pRange){
  134. var r = mark;
  135. var n = r.node;
  136. n.selectionStart = r.start;
  137. n.selectionEnd = r.end;
  138. }else{
  139. sel.removeAllRanges();
  140. sel.addRange(mark);
  141. }
  142. }else{
  143. console.warn("No idea how to restore selection for this browser!");
  144. }
  145. }else if(_doc.selection && mark){
  146. //'IE' way.
  147. var rg;
  148. if(mark.pRange){
  149. rg = mark.range;
  150. }else if(dojo.isArray(mark)){
  151. rg = _doc.body.createControlRange();
  152. //rg.addElement does not have call/apply method, so can not call it directly
  153. //rg is not available in "range.addElement(item)", so can't use that either
  154. dojo.forEach(mark, function(n){
  155. rg.addElement(n);
  156. });
  157. }else{
  158. rg = _doc.body.createTextRange();
  159. rg.moveToBookmark(mark);
  160. }
  161. rg.select();
  162. }
  163. }
  164. },
  165. getFocus: function(/*Widget?*/ menu, /*Window?*/ openedForWindow){
  166. // summary:
  167. // Called as getFocus(), this returns an Object showing the current focus
  168. // and selected text.
  169. //
  170. // Called as getFocus(widget), where widget is a (widget representing) a button
  171. // that was just pressed, it returns where focus was before that button
  172. // was pressed. (Pressing the button may have either shifted focus to the button,
  173. // or removed focus altogether.) In this case the selected text is not returned,
  174. // since it can't be accurately determined.
  175. //
  176. // menu: dijit._Widget or {domNode: DomNode} structure
  177. // The button that was just pressed. If focus has disappeared or moved
  178. // to this button, returns the previous focus. In this case the bookmark
  179. // information is already lost, and null is returned.
  180. //
  181. // openedForWindow:
  182. // iframe in which menu was opened
  183. //
  184. // returns:
  185. // A handle to restore focus/selection, to be passed to `dijit.focus`
  186. var node = !dijit._curFocus || (menu && dojo.isDescendant(dijit._curFocus, menu.domNode)) ? dijit._prevFocus : dijit._curFocus;
  187. return {
  188. node: node,
  189. bookmark: (node == dijit._curFocus) && dojo.withGlobal(openedForWindow || dojo.global, dijit.getBookmark),
  190. openedForWindow: openedForWindow
  191. }; // Object
  192. },
  193. focus: function(/*Object || DomNode */ handle){
  194. // summary:
  195. // Sets the focused node and the selection according to argument.
  196. // To set focus to an iframe's content, pass in the iframe itself.
  197. // handle:
  198. // object returned by get(), or a DomNode
  199. if(!handle){ return; }
  200. var node = "node" in handle ? handle.node : handle, // because handle is either DomNode or a composite object
  201. bookmark = handle.bookmark,
  202. openedForWindow = handle.openedForWindow,
  203. collapsed = bookmark ? bookmark.isCollapsed : false;
  204. // Set the focus
  205. // Note that for iframe's we need to use the <iframe> to follow the parentNode chain,
  206. // but we need to set focus to iframe.contentWindow
  207. if(node){
  208. var focusNode = (node.tagName.toLowerCase() == "iframe") ? node.contentWindow : node;
  209. if(focusNode && focusNode.focus){
  210. try{
  211. // Gecko throws sometimes if setting focus is impossible,
  212. // node not displayed or something like that
  213. focusNode.focus();
  214. }catch(e){/*quiet*/}
  215. }
  216. dijit._onFocusNode(node);
  217. }
  218. // set the selection
  219. // do not need to restore if current selection is not empty
  220. // (use keyboard to select a menu item) or if previous selection was collapsed
  221. // as it may cause focus shift (Esp in IE).
  222. if(bookmark && dojo.withGlobal(openedForWindow || dojo.global, dijit.isCollapsed) && !collapsed){
  223. if(openedForWindow){
  224. openedForWindow.focus();
  225. }
  226. try{
  227. dojo.withGlobal(openedForWindow || dojo.global, dijit.moveToBookmark, null, [bookmark]);
  228. }catch(e2){
  229. /*squelch IE internal error, see http://trac.dojotoolkit.org/ticket/1984 */
  230. }
  231. }
  232. },
  233. // _activeStack: dijit._Widget[]
  234. // List of currently active widgets (focused widget and it's ancestors)
  235. _activeStack: [],
  236. registerIframe: function(/*DomNode*/ iframe){
  237. // summary:
  238. // Registers listeners on the specified iframe so that any click
  239. // or focus event on that iframe (or anything in it) is reported
  240. // as a focus/click event on the <iframe> itself.
  241. // description:
  242. // Currently only used by editor.
  243. // returns:
  244. // Handle to pass to unregisterIframe()
  245. return dijit.registerWin(iframe.contentWindow, iframe);
  246. },
  247. unregisterIframe: function(/*Object*/ handle){
  248. // summary:
  249. // Unregisters listeners on the specified iframe created by registerIframe.
  250. // After calling be sure to delete or null out the handle itself.
  251. // handle:
  252. // Handle returned by registerIframe()
  253. dijit.unregisterWin(handle);
  254. },
  255. registerWin: function(/*Window?*/targetWindow, /*DomNode?*/ effectiveNode){
  256. // summary:
  257. // Registers listeners on the specified window (either the main
  258. // window or an iframe's window) to detect when the user has clicked somewhere
  259. // or focused somewhere.
  260. // description:
  261. // Users should call registerIframe() instead of this method.
  262. // targetWindow:
  263. // If specified this is the window associated with the iframe,
  264. // i.e. iframe.contentWindow.
  265. // effectiveNode:
  266. // If specified, report any focus events inside targetWindow as
  267. // an event on effectiveNode, rather than on evt.target.
  268. // returns:
  269. // Handle to pass to unregisterWin()
  270. // TODO: make this function private in 2.0; Editor/users should call registerIframe(),
  271. var mousedownListener = function(evt){
  272. dijit._justMouseDowned = true;
  273. setTimeout(function(){ dijit._justMouseDowned = false; }, 0);
  274. // workaround weird IE bug where the click is on an orphaned node
  275. // (first time clicking a Select/DropDownButton inside a TooltipDialog)
  276. if(dojo.isIE && evt && evt.srcElement && evt.srcElement.parentNode == null){
  277. return;
  278. }
  279. dijit._onTouchNode(effectiveNode || evt.target || evt.srcElement, "mouse");
  280. };
  281. //dojo.connect(targetWindow, "onscroll", ???);
  282. // Listen for blur and focus events on targetWindow's document.
  283. // IIRC, I'm using attachEvent() rather than dojo.connect() because focus/blur events don't bubble
  284. // through dojo.connect(), and also maybe to catch the focus events early, before onfocus handlers
  285. // fire.
  286. // Connect to <html> (rather than document) on IE to avoid memory leaks, but document on other browsers because
  287. // (at least for FF) the focus event doesn't fire on <html> or <body>.
  288. var doc = dojo.isIE ? targetWindow.document.documentElement : targetWindow.document;
  289. if(doc){
  290. if(dojo.isIE){
  291. targetWindow.document.body.attachEvent('onmousedown', mousedownListener);
  292. var activateListener = function(evt){
  293. // IE reports that nodes like <body> have gotten focus, even though they have tabIndex=-1,
  294. // Should consider those more like a mouse-click than a focus....
  295. if(evt.srcElement.tagName.toLowerCase() != "#document" &&
  296. dijit.isTabNavigable(evt.srcElement)){
  297. dijit._onFocusNode(effectiveNode || evt.srcElement);
  298. }else{
  299. dijit._onTouchNode(effectiveNode || evt.srcElement);
  300. }
  301. };
  302. doc.attachEvent('onactivate', activateListener);
  303. var deactivateListener = function(evt){
  304. dijit._onBlurNode(effectiveNode || evt.srcElement);
  305. };
  306. doc.attachEvent('ondeactivate', deactivateListener);
  307. return function(){
  308. targetWindow.document.detachEvent('onmousedown', mousedownListener);
  309. doc.detachEvent('onactivate', activateListener);
  310. doc.detachEvent('ondeactivate', deactivateListener);
  311. doc = null; // prevent memory leak (apparent circular reference via closure)
  312. };
  313. }else if(doc.body){
  314. doc.body.addEventListener('mousedown', mousedownListener, true);
  315. var focusListener = function(evt){
  316. dijit._onFocusNode(effectiveNode || evt.target);
  317. };
  318. doc.addEventListener('focus', focusListener, true);
  319. var blurListener = function(evt){
  320. dijit._onBlurNode(effectiveNode || evt.target);
  321. };
  322. doc.addEventListener('blur', blurListener, true);
  323. return function(){
  324. doc.body.removeEventListener('mousedown', mousedownListener, true);
  325. doc.removeEventListener('focus', focusListener, true);
  326. doc.removeEventListener('blur', blurListener, true);
  327. doc = null; // prevent memory leak (apparent circular reference via closure)
  328. };
  329. }
  330. }
  331. },
  332. unregisterWin: function(/*Handle*/ handle){
  333. // summary:
  334. // Unregisters listeners on the specified window (either the main
  335. // window or an iframe's window) according to handle returned from registerWin().
  336. // After calling be sure to delete or null out the handle itself.
  337. // Currently our handle is actually a function
  338. handle && handle();
  339. },
  340. _onBlurNode: function(/*DomNode*/ node){
  341. // summary:
  342. // Called when focus leaves a node.
  343. // Usually ignored, _unless_ it *isn't* follwed by touching another node,
  344. // which indicates that we tabbed off the last field on the page,
  345. // in which case every widget is marked inactive
  346. dijit._prevFocus = dijit._curFocus;
  347. dijit._curFocus = null;
  348. if(dijit._justMouseDowned){
  349. // the mouse down caused a new widget to be marked as active; this blur event
  350. // is coming late, so ignore it.
  351. return;
  352. }
  353. // if the blur event isn't followed by a focus event then mark all widgets as inactive.
  354. if(dijit._clearActiveWidgetsTimer){
  355. clearTimeout(dijit._clearActiveWidgetsTimer);
  356. }
  357. dijit._clearActiveWidgetsTimer = setTimeout(function(){
  358. delete dijit._clearActiveWidgetsTimer;
  359. dijit._setStack([]);
  360. dijit._prevFocus = null;
  361. }, 100);
  362. },
  363. _onTouchNode: function(/*DomNode*/ node, /*String*/ by){
  364. // summary:
  365. // Callback when node is focused or mouse-downed
  366. // node:
  367. // The node that was touched.
  368. // by:
  369. // "mouse" if the focus/touch was caused by a mouse down event
  370. // ignore the recent blurNode event
  371. if(dijit._clearActiveWidgetsTimer){
  372. clearTimeout(dijit._clearActiveWidgetsTimer);
  373. delete dijit._clearActiveWidgetsTimer;
  374. }
  375. // compute stack of active widgets (ex: ComboButton --> Menu --> MenuItem)
  376. var newStack=[];
  377. try{
  378. while(node){
  379. var popupParent = dojo.attr(node, "dijitPopupParent");
  380. if(popupParent){
  381. node=dijit.byId(popupParent).domNode;
  382. }else if(node.tagName && node.tagName.toLowerCase() == "body"){
  383. // is this the root of the document or just the root of an iframe?
  384. if(node === dojo.body()){
  385. // node is the root of the main document
  386. break;
  387. }
  388. // otherwise, find the iframe this node refers to (can't access it via parentNode,
  389. // need to do this trick instead). window.frameElement is supported in IE/FF/Webkit
  390. node=dojo.window.get(node.ownerDocument).frameElement;
  391. }else{
  392. // if this node is the root node of a widget, then add widget id to stack,
  393. // except ignore clicks on disabled widgets (actually focusing a disabled widget still works,
  394. // to support MenuItem)
  395. var id = node.getAttribute && node.getAttribute("widgetId"),
  396. widget = id && dijit.byId(id);
  397. if(widget && !(by == "mouse" && widget.get("disabled"))){
  398. newStack.unshift(id);
  399. }
  400. node=node.parentNode;
  401. }
  402. }
  403. }catch(e){ /* squelch */ }
  404. dijit._setStack(newStack, by);
  405. },
  406. _onFocusNode: function(/*DomNode*/ node){
  407. // summary:
  408. // Callback when node is focused
  409. if(!node){
  410. return;
  411. }
  412. if(node.nodeType == 9){
  413. // Ignore focus events on the document itself. This is here so that
  414. // (for example) clicking the up/down arrows of a spinner
  415. // (which don't get focus) won't cause that widget to blur. (FF issue)
  416. return;
  417. }
  418. dijit._onTouchNode(node);
  419. if(node == dijit._curFocus){ return; }
  420. if(dijit._curFocus){
  421. dijit._prevFocus = dijit._curFocus;
  422. }
  423. dijit._curFocus = node;
  424. dojo.publish("focusNode", [node]);
  425. },
  426. _setStack: function(/*String[]*/ newStack, /*String*/ by){
  427. // summary:
  428. // The stack of active widgets has changed. Send out appropriate events and records new stack.
  429. // newStack:
  430. // array of widget id's, starting from the top (outermost) widget
  431. // by:
  432. // "mouse" if the focus/touch was caused by a mouse down event
  433. var oldStack = dijit._activeStack;
  434. dijit._activeStack = newStack;
  435. // compare old stack to new stack to see how many elements they have in common
  436. for(var nCommon=0; nCommon<Math.min(oldStack.length, newStack.length); nCommon++){
  437. if(oldStack[nCommon] != newStack[nCommon]){
  438. break;
  439. }
  440. }
  441. var widget;
  442. // for all elements that have gone out of focus, send blur event
  443. for(var i=oldStack.length-1; i>=nCommon; i--){
  444. widget = dijit.byId(oldStack[i]);
  445. if(widget){
  446. widget._focused = false;
  447. widget.set("focused", false);
  448. widget._hasBeenBlurred = true;
  449. if(widget._onBlur){
  450. widget._onBlur(by);
  451. }
  452. dojo.publish("widgetBlur", [widget, by]);
  453. }
  454. }
  455. // for all element that have come into focus, send focus event
  456. for(i=nCommon; i<newStack.length; i++){
  457. widget = dijit.byId(newStack[i]);
  458. if(widget){
  459. widget._focused = true;
  460. widget.set("focused", true);
  461. if(widget._onFocus){
  462. widget._onFocus(by);
  463. }
  464. dojo.publish("widgetFocus", [widget, by]);
  465. }
  466. }
  467. }
  468. });
  469. // register top window and all the iframes it contains
  470. dojo.addOnLoad(function(){
  471. var handle = dijit.registerWin(window);
  472. if(dojo.isIE){
  473. dojo.addOnWindowUnload(function(){
  474. dijit.unregisterWin(handle);
  475. handle = null;
  476. })
  477. }
  478. });
  479. }