Menu.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. define("dijit/Menu", [
  2. "require",
  3. "dojo/_base/array", // array.forEach
  4. "dojo/_base/declare", // declare
  5. "dojo/_base/event", // event.stop
  6. "dojo/dom", // dom.byId dom.isDescendant
  7. "dojo/dom-attr", // domAttr.get domAttr.set domAttr.has domAttr.remove
  8. "dojo/dom-geometry", // domStyle.getComputedStyle domGeometry.position
  9. "dojo/dom-style", // domStyle.getComputedStyle
  10. "dojo/_base/kernel",
  11. "dojo/keys", // keys.F10
  12. "dojo/_base/lang", // lang.hitch
  13. "dojo/on",
  14. "dojo/_base/sniff", // has("ie"), has("quirks")
  15. "dojo/_base/window", // win.body win.doc.documentElement win.doc.frames win.withGlobal
  16. "dojo/window", // winUtils.get
  17. "./popup",
  18. "./DropDownMenu",
  19. "dojo/ready"
  20. ], function(require, array, declare, event, dom, domAttr, domGeometry, domStyle, kernel, keys, lang, on,
  21. has, win, winUtils, pm, DropDownMenu, ready){
  22. /*=====
  23. var DropDownMenu = dijit.DropDownMenu;
  24. =====*/
  25. // module:
  26. // dijit/Menu
  27. // summary:
  28. // Includes dijit.Menu widget and base class dijit._MenuBase
  29. // Back compat w/1.6, remove for 2.0
  30. if(!kernel.isAsync){
  31. ready(0, function(){
  32. var requires = ["dijit/MenuItem", "dijit/PopupMenuItem", "dijit/CheckedMenuItem", "dijit/MenuSeparator"];
  33. require(requires); // use indirection so modules not rolled into a build
  34. });
  35. }
  36. return declare("dijit.Menu", DropDownMenu, {
  37. // summary:
  38. // A context menu you can assign to multiple elements
  39. constructor: function(){
  40. this._bindings = [];
  41. },
  42. // targetNodeIds: [const] String[]
  43. // Array of dom node ids of nodes to attach to.
  44. // Fill this with nodeIds upon widget creation and it becomes context menu for those nodes.
  45. targetNodeIds: [],
  46. // contextMenuForWindow: [const] Boolean
  47. // If true, right clicking anywhere on the window will cause this context menu to open.
  48. // If false, must specify targetNodeIds.
  49. contextMenuForWindow: false,
  50. // leftClickToOpen: [const] Boolean
  51. // If true, menu will open on left click instead of right click, similar to a file menu.
  52. leftClickToOpen: false,
  53. // refocus: Boolean
  54. // When this menu closes, re-focus the element which had focus before it was opened.
  55. refocus: true,
  56. postCreate: function(){
  57. if(this.contextMenuForWindow){
  58. this.bindDomNode(win.body());
  59. }else{
  60. // TODO: should have _setTargetNodeIds() method to handle initialization and a possible
  61. // later set('targetNodeIds', ...) call. There's also a problem that targetNodeIds[]
  62. // gets stale after calls to bindDomNode()/unBindDomNode() as it still is just the original list (see #9610)
  63. array.forEach(this.targetNodeIds, this.bindDomNode, this);
  64. }
  65. this.inherited(arguments);
  66. },
  67. // thanks burstlib!
  68. _iframeContentWindow: function(/* HTMLIFrameElement */iframe_el){
  69. // summary:
  70. // Returns the window reference of the passed iframe
  71. // tags:
  72. // private
  73. return winUtils.get(this._iframeContentDocument(iframe_el)) ||
  74. // Moz. TODO: is this available when defaultView isn't?
  75. this._iframeContentDocument(iframe_el)['__parent__'] ||
  76. (iframe_el.name && win.doc.frames[iframe_el.name]) || null; // Window
  77. },
  78. _iframeContentDocument: function(/* HTMLIFrameElement */iframe_el){
  79. // summary:
  80. // Returns a reference to the document object inside iframe_el
  81. // tags:
  82. // protected
  83. return iframe_el.contentDocument // W3
  84. || (iframe_el.contentWindow && iframe_el.contentWindow.document) // IE
  85. || (iframe_el.name && win.doc.frames[iframe_el.name] && win.doc.frames[iframe_el.name].document)
  86. || null; // HTMLDocument
  87. },
  88. bindDomNode: function(/*String|DomNode*/ node){
  89. // summary:
  90. // Attach menu to given node
  91. node = dom.byId(node);
  92. var cn; // Connect node
  93. // Support context menus on iframes. Rather than binding to the iframe itself we need
  94. // to bind to the <body> node inside the iframe.
  95. if(node.tagName.toLowerCase() == "iframe"){
  96. var iframe = node,
  97. window = this._iframeContentWindow(iframe);
  98. cn = win.withGlobal(window, win.body);
  99. }else{
  100. // To capture these events at the top level, attach to <html>, not <body>.
  101. // Otherwise right-click context menu just doesn't work.
  102. cn = (node == win.body() ? win.doc.documentElement : node);
  103. }
  104. // "binding" is the object to track our connection to the node (ie, the parameter to bindDomNode())
  105. var binding = {
  106. node: node,
  107. iframe: iframe
  108. };
  109. // Save info about binding in _bindings[], and make node itself record index(+1) into
  110. // _bindings[] array. Prefix w/_dijitMenu to avoid setting an attribute that may
  111. // start with a number, which fails on FF/safari.
  112. domAttr.set(node, "_dijitMenu" + this.id, this._bindings.push(binding));
  113. // Setup the connections to monitor click etc., unless we are connecting to an iframe which hasn't finished
  114. // loading yet, in which case we need to wait for the onload event first, and then connect
  115. // On linux Shift-F10 produces the oncontextmenu event, but on Windows it doesn't, so
  116. // we need to monitor keyboard events in addition to the oncontextmenu event.
  117. var doConnects = lang.hitch(this, function(cn){
  118. return [
  119. // TODO: when leftClickToOpen is true then shouldn't space/enter key trigger the menu,
  120. // rather than shift-F10?
  121. on(cn, this.leftClickToOpen ? "click" : "contextmenu", lang.hitch(this, function(evt){
  122. // Schedule context menu to be opened unless it's already been scheduled from onkeydown handler
  123. event.stop(evt);
  124. this._scheduleOpen(evt.target, iframe, {x: evt.pageX, y: evt.pageY});
  125. })),
  126. on(cn, "keydown", lang.hitch(this, function(evt){
  127. if(evt.shiftKey && evt.keyCode == keys.F10){
  128. event.stop(evt);
  129. this._scheduleOpen(evt.target, iframe); // no coords - open near target node
  130. }
  131. }))
  132. ];
  133. });
  134. binding.connects = cn ? doConnects(cn) : [];
  135. if(iframe){
  136. // Setup handler to [re]bind to the iframe when the contents are initially loaded,
  137. // and every time the contents change.
  138. // Need to do this b/c we are actually binding to the iframe's <body> node.
  139. // Note: can't use connect.connect(), see #9609.
  140. binding.onloadHandler = lang.hitch(this, function(){
  141. // want to remove old connections, but IE throws exceptions when trying to
  142. // access the <body> node because it's already gone, or at least in a state of limbo
  143. var window = this._iframeContentWindow(iframe);
  144. cn = win.withGlobal(window, win.body);
  145. binding.connects = doConnects(cn);
  146. });
  147. if(iframe.addEventListener){
  148. iframe.addEventListener("load", binding.onloadHandler, false);
  149. }else{
  150. iframe.attachEvent("onload", binding.onloadHandler);
  151. }
  152. }
  153. },
  154. unBindDomNode: function(/*String|DomNode*/ nodeName){
  155. // summary:
  156. // Detach menu from given node
  157. var node;
  158. try{
  159. node = dom.byId(nodeName);
  160. }catch(e){
  161. // On IE the dom.byId() call will get an exception if the attach point was
  162. // the <body> node of an <iframe> that has since been reloaded (and thus the
  163. // <body> node is in a limbo state of destruction.
  164. return;
  165. }
  166. // node["_dijitMenu" + this.id] contains index(+1) into my _bindings[] array
  167. var attrName = "_dijitMenu" + this.id;
  168. if(node && domAttr.has(node, attrName)){
  169. var bid = domAttr.get(node, attrName)-1, b = this._bindings[bid], h;
  170. while(h = b.connects.pop()){
  171. h.remove();
  172. }
  173. // Remove listener for iframe onload events
  174. var iframe = b.iframe;
  175. if(iframe){
  176. if(iframe.removeEventListener){
  177. iframe.removeEventListener("load", b.onloadHandler, false);
  178. }else{
  179. iframe.detachEvent("onload", b.onloadHandler);
  180. }
  181. }
  182. domAttr.remove(node, attrName);
  183. delete this._bindings[bid];
  184. }
  185. },
  186. _scheduleOpen: function(/*DomNode?*/ target, /*DomNode?*/ iframe, /*Object?*/ coords){
  187. // summary:
  188. // Set timer to display myself. Using a timer rather than displaying immediately solves
  189. // two problems:
  190. //
  191. // 1. IE: without the delay, focus work in "open" causes the system
  192. // context menu to appear in spite of stopEvent.
  193. //
  194. // 2. Avoid double-shows on linux, where shift-F10 generates an oncontextmenu event
  195. // even after a event.stop(e). (Shift-F10 on windows doesn't generate the
  196. // oncontextmenu event.)
  197. if(!this._openTimer){
  198. this._openTimer = setTimeout(lang.hitch(this, function(){
  199. delete this._openTimer;
  200. this._openMyself({
  201. target: target,
  202. iframe: iframe,
  203. coords: coords
  204. });
  205. }), 1);
  206. }
  207. },
  208. _openMyself: function(args){
  209. // summary:
  210. // Internal function for opening myself when the user does a right-click or something similar.
  211. // args:
  212. // This is an Object containing:
  213. // * target:
  214. // The node that is being clicked
  215. // * iframe:
  216. // If an <iframe> is being clicked, iframe points to that iframe
  217. // * coords:
  218. // Put menu at specified x/y position in viewport, or if iframe is
  219. // specified, then relative to iframe.
  220. //
  221. // _openMyself() formerly took the event object, and since various code references
  222. // evt.target (after connecting to _openMyself()), using an Object for parameters
  223. // (so that old code still works).
  224. var target = args.target,
  225. iframe = args.iframe,
  226. coords = args.coords;
  227. // Get coordinates to open menu, either at specified (mouse) position or (if triggered via keyboard)
  228. // then near the node the menu is assigned to.
  229. if(coords){
  230. if(iframe){
  231. // Specified coordinates are on <body> node of an <iframe>, convert to match main document
  232. var ifc = domGeometry.position(iframe, true),
  233. window = this._iframeContentWindow(iframe),
  234. scroll = win.withGlobal(window, "_docScroll", dojo);
  235. var cs = domStyle.getComputedStyle(iframe),
  236. tp = domStyle.toPixelValue,
  237. left = (has("ie") && has("quirks") ? 0 : tp(iframe, cs.paddingLeft)) + (has("ie") && has("quirks") ? tp(iframe, cs.borderLeftWidth) : 0),
  238. top = (has("ie") && has("quirks") ? 0 : tp(iframe, cs.paddingTop)) + (has("ie") && has("quirks") ? tp(iframe, cs.borderTopWidth) : 0);
  239. coords.x += ifc.x + left - scroll.x;
  240. coords.y += ifc.y + top - scroll.y;
  241. }
  242. }else{
  243. coords = domGeometry.position(target, true);
  244. coords.x += 10;
  245. coords.y += 10;
  246. }
  247. var self=this;
  248. var prevFocusNode = this._focusManager.get("prevNode");
  249. var curFocusNode = this._focusManager.get("curNode");
  250. var savedFocusNode = !curFocusNode || (dom.isDescendant(curFocusNode, this.domNode)) ? prevFocusNode : curFocusNode;
  251. function closeAndRestoreFocus(){
  252. // user has clicked on a menu or popup
  253. if(self.refocus && savedFocusNode){
  254. savedFocusNode.focus();
  255. }
  256. pm.close(self);
  257. }
  258. pm.open({
  259. popup: this,
  260. x: coords.x,
  261. y: coords.y,
  262. onExecute: closeAndRestoreFocus,
  263. onCancel: closeAndRestoreFocus,
  264. orient: this.isLeftToRight() ? 'L' : 'R'
  265. });
  266. this.focus();
  267. this._onBlur = function(){
  268. this.inherited('_onBlur', arguments);
  269. // Usually the parent closes the child widget but if this is a context
  270. // menu then there is no parent
  271. pm.close(this);
  272. // don't try to restore focus; user has clicked another part of the screen
  273. // and set focus there
  274. };
  275. },
  276. uninitialize: function(){
  277. array.forEach(this._bindings, function(b){ if(b){ this.unBindDomNode(b.node); } }, this);
  278. this.inherited(arguments);
  279. }
  280. });
  281. });