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