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