source: Dev/branches/rest-dojo-ui/client/dijit/Menu.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

File size: 10.9 KB
RevLine 
[256]1define([
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
33if(!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
40return 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});
Note: See TracBrowser for help on using the repository browser.