]> git.wh0rd.org - tt-rss.git/blob - lib/dijit/Menu.js.uncompressed.js
upgrade dojo to 1.8.3 (refs #570)
[tt-rss.git] / lib / dijit / Menu.js.uncompressed.js
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 });