source: Dev/trunk/src/client/dijit/Menu.js @ 525

Last change on this file since 525 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

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