Menu.js.uncompressed.js 12 KB

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