|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 // Positioning buffer enforced between the edge of a context menu |
|
6 // and the edge of the screen. |
|
7 const kPositionPadding = 10; |
|
8 |
|
9 var AutofillMenuUI = { |
|
10 _popupState: null, |
|
11 __menuPopup: null, |
|
12 |
|
13 get _panel() { return document.getElementById("autofill-container"); }, |
|
14 get _popup() { return document.getElementById("autofill-popup"); }, |
|
15 get commands() { return this._popup.childNodes[0]; }, |
|
16 |
|
17 get _menuPopup() { |
|
18 if (!this.__menuPopup) { |
|
19 this.__menuPopup = new MenuPopup(this._panel, this._popup); |
|
20 this.__menuPopup._wantTypeBehind = true; |
|
21 this.__menuPopup.controller = this; |
|
22 } |
|
23 return this.__menuPopup; |
|
24 }, |
|
25 |
|
26 _firePopupEvent: function _firePopupEvent(aEventName) { |
|
27 let menupopup = this._currentControl.menupopup; |
|
28 if (menupopup.hasAttribute(aEventName)) { |
|
29 let func = new Function("event", menupopup.getAttribute(aEventName)); |
|
30 func.call(this); |
|
31 } |
|
32 }, |
|
33 |
|
34 _emptyCommands: function _emptyCommands() { |
|
35 while (this.commands.firstChild) |
|
36 this.commands.removeChild(this.commands.firstChild); |
|
37 }, |
|
38 |
|
39 _positionOptions: function _positionOptions() { |
|
40 return { |
|
41 bottomAligned: false, |
|
42 leftAligned: true, |
|
43 xPos: this._anchorRect.x, |
|
44 yPos: this._anchorRect.y + this._anchorRect.height, |
|
45 maxWidth: this._anchorRect.width, |
|
46 maxHeight: 350, |
|
47 source: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH |
|
48 }; |
|
49 }, |
|
50 |
|
51 show: function show(aAnchorRect, aSuggestionsList) { |
|
52 this.commands.addEventListener("select", this, true); |
|
53 |
|
54 this._anchorRect = aAnchorRect; |
|
55 this._emptyCommands(); |
|
56 for (let idx = 0; idx < aSuggestionsList.length; idx++) { |
|
57 let item = document.createElement("richlistitem"); |
|
58 let label = document.createElement("label"); |
|
59 label.setAttribute("value", aSuggestionsList[idx].label); |
|
60 item.setAttribute("value", aSuggestionsList[idx].value); |
|
61 item.setAttribute("data", aSuggestionsList[idx].value); |
|
62 item.appendChild(label); |
|
63 this.commands.appendChild(item); |
|
64 } |
|
65 this._menuPopup.show(this._positionOptions()); |
|
66 }, |
|
67 |
|
68 selectByIndex: function mn_selectByIndex(aIndex) { |
|
69 this._menuPopup.hide(); |
|
70 FormHelperUI.doAutoComplete(this.commands.childNodes[aIndex].getAttribute("data")); |
|
71 }, |
|
72 |
|
73 hide: function hide () { |
|
74 this.commands.removeEventListener("select", this, true); |
|
75 |
|
76 this._menuPopup.hide(); |
|
77 }, |
|
78 |
|
79 handleEvent: function (aEvent) { |
|
80 switch (aEvent.type) { |
|
81 case "select": |
|
82 FormHelperUI.doAutoComplete(this.commands.value); |
|
83 break; |
|
84 } |
|
85 } |
|
86 }; |
|
87 |
|
88 var ContextMenuUI = { |
|
89 _popupState: null, |
|
90 __menuPopup: null, |
|
91 _defaultPositionOptions: { |
|
92 bottomAligned: true, |
|
93 rightAligned: false, |
|
94 centerHorizontally: true, |
|
95 moveBelowToFit: true |
|
96 }, |
|
97 |
|
98 get _panel() { return document.getElementById("context-container"); }, |
|
99 get _popup() { return document.getElementById("context-popup"); }, |
|
100 get commands() { return this._popup.childNodes[0]; }, |
|
101 |
|
102 get _menuPopup() { |
|
103 if (!this.__menuPopup) { |
|
104 this.__menuPopup = new MenuPopup(this._panel, this._popup); |
|
105 this.__menuPopup.controller = this; |
|
106 } |
|
107 return this.__menuPopup; |
|
108 }, |
|
109 |
|
110 /******************************************* |
|
111 * External api |
|
112 */ |
|
113 |
|
114 /* |
|
115 * popupState - return the json object for this context. Called |
|
116 * by context command to invoke actions on the target. |
|
117 */ |
|
118 get popupState() { |
|
119 return this._popupState; |
|
120 }, |
|
121 |
|
122 /* |
|
123 * showContextMenu - display a context sensitive menu based |
|
124 * on the data provided in a json data structure. |
|
125 * |
|
126 * @param aMessage data structure containing information about |
|
127 * the context. |
|
128 * aMessage.json - json data structure described below. |
|
129 * aMessage.target - target element on which to evoke |
|
130 * |
|
131 * @returns true if the context menu was displayed, |
|
132 * false otherwise. |
|
133 * |
|
134 * json: TBD |
|
135 */ |
|
136 showContextMenu: function ch_showContextMenu(aMessage) { |
|
137 this._popupState = aMessage.json; |
|
138 this._popupState.target = aMessage.target; |
|
139 let contentTypes = this._popupState.types; |
|
140 |
|
141 /* |
|
142 * Types in ContextMenuHandler: |
|
143 * image |
|
144 * link |
|
145 * input-text - generic form input control |
|
146 * copy - form input that has some selected text |
|
147 * selectable - form input with text that can be selected |
|
148 * input-empty - form input (empty) |
|
149 * paste - form input and there's text on the clipboard |
|
150 * selected-text - generic content text that is selected |
|
151 * content-text - generic content text |
|
152 * video |
|
153 * media-paused, media-playing |
|
154 * paste-url - url bar w/text on the clipboard |
|
155 */ |
|
156 |
|
157 Util.dumpLn("contentTypes:", contentTypes); |
|
158 |
|
159 // Defines whether or not low priority items in images, text, and |
|
160 // links are displayed. |
|
161 let multipleMediaTypes = false; |
|
162 if (contentTypes.indexOf("link") != -1 && |
|
163 (contentTypes.indexOf("image") != -1 || |
|
164 contentTypes.indexOf("video") != -1 || |
|
165 contentTypes.indexOf("selected-text") != -1)) |
|
166 multipleMediaTypes = true; |
|
167 |
|
168 for (let command of Array.slice(this.commands.childNodes)) { |
|
169 command.hidden = true; |
|
170 command.selected = false; |
|
171 } |
|
172 |
|
173 let optionsAvailable = false; |
|
174 for (let command of Array.slice(this.commands.childNodes)) { |
|
175 let types = command.getAttribute("type").split(","); |
|
176 let lowPriority = (command.hasAttribute("priority") && |
|
177 command.getAttribute("priority") == "low"); |
|
178 let searchTextItem = (command.id == "context-search"); |
|
179 |
|
180 // filter low priority items if we have more than one media type. |
|
181 if (multipleMediaTypes && lowPriority) |
|
182 continue; |
|
183 |
|
184 for (let i = 0; i < types.length; i++) { |
|
185 // If one of the item's types has '!' before it, treat it as an exclusion rule. |
|
186 if (types[i].charAt(0) == '!' && contentTypes.indexOf(types[i].substring(1)) != -1) { |
|
187 break; |
|
188 } |
|
189 if (contentTypes.indexOf(types[i]) != -1) { |
|
190 // If this is the special search text item, we need to set its label dynamically. |
|
191 if (searchTextItem && !ContextCommands.searchTextSetup(command, this._popupState.string)) { |
|
192 break; |
|
193 } |
|
194 optionsAvailable = true; |
|
195 command.hidden = false; |
|
196 break; |
|
197 } |
|
198 } |
|
199 } |
|
200 |
|
201 if (!optionsAvailable) { |
|
202 this._popupState = null; |
|
203 return false; |
|
204 } |
|
205 |
|
206 let coords = { x: aMessage.json.xPos, y: aMessage.json.yPos }; |
|
207 |
|
208 // chrome calls don't need to be translated and as such |
|
209 // don't provide target. |
|
210 if (aMessage.target && aMessage.target.localName === "browser") { |
|
211 coords = aMessage.target.msgBrowserToClient(aMessage, true); |
|
212 } |
|
213 this._menuPopup.show(Util.extend({}, this._defaultPositionOptions, { |
|
214 xPos: coords.x, |
|
215 yPos: coords.y, |
|
216 source: aMessage.json.source |
|
217 })); |
|
218 return true; |
|
219 }, |
|
220 |
|
221 hide: function hide () { |
|
222 for (let command of this.commands.querySelectorAll("richlistitem[selected]")) { |
|
223 command.removeAttribute("selected"); |
|
224 } |
|
225 this._menuPopup.hide(); |
|
226 this._popupState = null; |
|
227 }, |
|
228 |
|
229 reset: function reset() { |
|
230 this._popupState = null; |
|
231 } |
|
232 }; |
|
233 |
|
234 var MenuControlUI = { |
|
235 _currentControl: null, |
|
236 __menuPopup: null, |
|
237 |
|
238 get _panel() { return document.getElementById("menucontrol-container"); }, |
|
239 get _popup() { return document.getElementById("menucontrol-popup"); }, |
|
240 get commands() { return this._popup.childNodes[0]; }, |
|
241 |
|
242 get _menuPopup() { |
|
243 if (!this.__menuPopup) { |
|
244 this.__menuPopup = new MenuPopup(this._panel, this._popup); |
|
245 this.__menuPopup.controller = this; |
|
246 } |
|
247 return this.__menuPopup; |
|
248 }, |
|
249 |
|
250 _firePopupEvent: function _firePopupEvent(aEventName) { |
|
251 let menupopup = this._currentControl.menupopup; |
|
252 if (menupopup.hasAttribute(aEventName)) { |
|
253 let func = new Function("event", menupopup.getAttribute(aEventName)); |
|
254 func.call(this); |
|
255 } |
|
256 }, |
|
257 |
|
258 _emptyCommands: function _emptyCommands() { |
|
259 while (this.commands.firstChild) |
|
260 this.commands.removeChild(this.commands.firstChild); |
|
261 }, |
|
262 |
|
263 _positionOptions: function _positionOptions() { |
|
264 let position = this._currentControl.menupopup.position || "after_start"; |
|
265 let rect = this._currentControl.getBoundingClientRect(); |
|
266 |
|
267 let options = {}; |
|
268 |
|
269 // TODO: Detect text direction and flip for RTL. |
|
270 |
|
271 switch (position) { |
|
272 case "before_start": |
|
273 options.xPos = rect.left; |
|
274 options.yPos = rect.top; |
|
275 options.bottomAligned = true; |
|
276 options.leftAligned = true; |
|
277 break; |
|
278 case "before_end": |
|
279 options.xPos = rect.right; |
|
280 options.yPos = rect.top; |
|
281 options.bottomAligned = true; |
|
282 options.rightAligned = true; |
|
283 break; |
|
284 case "after_start": |
|
285 options.xPos = rect.left; |
|
286 options.yPos = rect.bottom; |
|
287 options.topAligned = true; |
|
288 options.leftAligned = true; |
|
289 break; |
|
290 case "after_end": |
|
291 options.xPos = rect.right; |
|
292 options.yPos = rect.bottom; |
|
293 options.topAligned = true; |
|
294 options.rightAligned = true; |
|
295 break; |
|
296 |
|
297 // TODO: Support other popup positions. |
|
298 } |
|
299 |
|
300 return options; |
|
301 }, |
|
302 |
|
303 show: function show(aMenuControl) { |
|
304 this._currentControl = aMenuControl; |
|
305 this._panel.setAttribute("for", aMenuControl.id); |
|
306 this._firePopupEvent("onpopupshowing"); |
|
307 |
|
308 this._emptyCommands(); |
|
309 let children = this._currentControl.menupopup.children; |
|
310 for (let i = 0; i < children.length; i++) { |
|
311 let child = children[i]; |
|
312 let item = document.createElement("richlistitem"); |
|
313 |
|
314 if (child.disabled) |
|
315 item.setAttribute("disabled", "true"); |
|
316 |
|
317 if (child.hidden) |
|
318 item.setAttribute("hidden", "true"); |
|
319 |
|
320 // Add selected as a class name instead of an attribute to not being overidden |
|
321 // by the richlistbox behavior (it sets the "current" and "selected" attribute |
|
322 if (child.selected) |
|
323 item.setAttribute("class", "selected"); |
|
324 |
|
325 let image = document.createElement("image"); |
|
326 image.setAttribute("src", child.image || ""); |
|
327 item.appendChild(image); |
|
328 |
|
329 let label = document.createElement("label"); |
|
330 label.setAttribute("value", child.label); |
|
331 item.appendChild(label); |
|
332 |
|
333 this.commands.appendChild(item); |
|
334 } |
|
335 |
|
336 this._menuPopup.show(this._positionOptions()); |
|
337 }, |
|
338 |
|
339 selectByIndex: function mn_selectByIndex(aIndex) { |
|
340 this._currentControl.selectedIndex = aIndex; |
|
341 |
|
342 // Dispatch a xul command event to the attached menulist |
|
343 if (this._currentControl.dispatchEvent) { |
|
344 let evt = document.createEvent("XULCommandEvent"); |
|
345 evt.initCommandEvent("command", true, true, window, 0, false, false, false, false, null); |
|
346 this._currentControl.dispatchEvent(evt); |
|
347 } |
|
348 |
|
349 this._menuPopup.hide(); |
|
350 } |
|
351 }; |
|
352 |
|
353 function MenuPopup(aPanel, aPopup) { |
|
354 this._panel = aPanel; |
|
355 this._popup = aPopup; |
|
356 this._wantTypeBehind = false; |
|
357 |
|
358 window.addEventListener('MozAppbarShowing', this, false); |
|
359 } |
|
360 MenuPopup.prototype = { |
|
361 get visible() { return !this._panel.hidden; }, |
|
362 get commands() { return this._popup.childNodes[0]; }, |
|
363 |
|
364 show: function (aPositionOptions) { |
|
365 if (!this.visible) { |
|
366 this._animateShow(aPositionOptions); |
|
367 } |
|
368 }, |
|
369 |
|
370 hide: function () { |
|
371 if (this.visible) { |
|
372 this._animateHide(); |
|
373 } |
|
374 }, |
|
375 |
|
376 _position: function _position(aPositionOptions) { |
|
377 let aX = aPositionOptions.xPos; |
|
378 let aY = aPositionOptions.yPos; |
|
379 let aSource = aPositionOptions.source; |
|
380 |
|
381 // Set these first so they are set when we do misc. calculations below. |
|
382 if (aPositionOptions.maxWidth) { |
|
383 this._popup.style.maxWidth = aPositionOptions.maxWidth + "px"; |
|
384 } |
|
385 if (aPositionOptions.maxHeight) { |
|
386 this._popup.style.maxHeight = aPositionOptions.maxHeight + "px"; |
|
387 } |
|
388 |
|
389 let width = this._popup.boxObject.width; |
|
390 let height = this._popup.boxObject.height; |
|
391 let halfWidth = width / 2; |
|
392 let screenWidth = ContentAreaObserver.width; |
|
393 let screenHeight = ContentAreaObserver.height; |
|
394 |
|
395 if (aPositionOptions.rightAligned) |
|
396 aX -= width; |
|
397 |
|
398 if (aPositionOptions.bottomAligned) |
|
399 aY -= height; |
|
400 |
|
401 if (aPositionOptions.centerHorizontally) |
|
402 aX -= halfWidth; |
|
403 |
|
404 // Always leave some padding. |
|
405 if (aX < kPositionPadding) { |
|
406 aX = kPositionPadding; |
|
407 } else if (aX + width + kPositionPadding > screenWidth){ |
|
408 // Don't let the popup overflow to the right. |
|
409 aX = Math.max(screenWidth - width - kPositionPadding, kPositionPadding); |
|
410 } |
|
411 |
|
412 if (aY < kPositionPadding && aPositionOptions.moveBelowToFit) { |
|
413 // show context menu below when it doesn't fit. |
|
414 aY = aPositionOptions.yPos; |
|
415 } |
|
416 |
|
417 if (aY < kPositionPadding) { |
|
418 aY = kPositionPadding; |
|
419 } else if (aY + height + kPositionPadding > screenHeight){ |
|
420 aY = Math.max(screenHeight - height - kPositionPadding, kPositionPadding); |
|
421 } |
|
422 |
|
423 this._panel.left = aX; |
|
424 this._panel.top = aY; |
|
425 |
|
426 if (!aPositionOptions.maxHeight) { |
|
427 // Make sure it fits in the window. |
|
428 let popupHeight = Math.min(aY + height + kPositionPadding, screenHeight - aY - kPositionPadding); |
|
429 this._popup.style.maxHeight = popupHeight + "px"; |
|
430 } |
|
431 |
|
432 if (!aPositionOptions.maxWidth) { |
|
433 let popupWidth = Math.min(aX + width + kPositionPadding, screenWidth - aX - kPositionPadding); |
|
434 this._popup.style.maxWidth = popupWidth + "px"; |
|
435 } |
|
436 }, |
|
437 |
|
438 _animateShow: function (aPositionOptions) { |
|
439 let deferred = Promise.defer(); |
|
440 |
|
441 window.addEventListener("keypress", this, true); |
|
442 window.addEventListener("mousedown", this, true); |
|
443 window.addEventListener("touchstart", this, true); |
|
444 window.addEventListener("scroll", this, true); |
|
445 window.addEventListener("blur", this, true); |
|
446 Elements.stack.addEventListener("PopupChanged", this, false); |
|
447 |
|
448 this._panel.hidden = false; |
|
449 let popupFrom = !aPositionOptions.bottomAligned ? "above" : "below"; |
|
450 this._panel.setAttribute("showingfrom", popupFrom); |
|
451 |
|
452 // This triggers a reflow, which sets transitionability. |
|
453 // All animation/transition setup must happen before here. |
|
454 this._position(aPositionOptions || {}); |
|
455 |
|
456 let self = this; |
|
457 this._panel.addEventListener("transitionend", function popupshown () { |
|
458 self._panel.removeEventListener("transitionend", popupshown); |
|
459 self._panel.removeAttribute("showingfrom"); |
|
460 |
|
461 self._dispatch("popupshown"); |
|
462 deferred.resolve(); |
|
463 }); |
|
464 |
|
465 this._panel.setAttribute("showing", "true"); |
|
466 return deferred.promise; |
|
467 }, |
|
468 |
|
469 _animateHide: function () { |
|
470 let deferred = Promise.defer(); |
|
471 |
|
472 window.removeEventListener("keypress", this, true); |
|
473 window.removeEventListener("mousedown", this, true); |
|
474 window.removeEventListener("touchstart", this, true); |
|
475 window.removeEventListener("scroll", this, true); |
|
476 window.removeEventListener("blur", this, true); |
|
477 Elements.stack.removeEventListener("PopupChanged", this, false); |
|
478 |
|
479 let self = this; |
|
480 this._panel.addEventListener("transitionend", function popuphidden() { |
|
481 self._panel.removeEventListener("transitionend", popuphidden); |
|
482 self._panel.removeAttribute("hiding"); |
|
483 self._panel.hidden = true; |
|
484 self._popup.style.maxWidth = "none"; |
|
485 self._popup.style.maxHeight = "none"; |
|
486 |
|
487 self._dispatch("popuphidden"); |
|
488 deferred.resolve(); |
|
489 }); |
|
490 |
|
491 this._panel.setAttribute("hiding", "true"); |
|
492 this._panel.removeAttribute("showing"); |
|
493 return deferred.promise; |
|
494 }, |
|
495 |
|
496 _dispatch: function _dispatch(aName) { |
|
497 let event = document.createEvent("Events"); |
|
498 event.initEvent(aName, true, false); |
|
499 this._panel.dispatchEvent(event); |
|
500 }, |
|
501 |
|
502 handleEvent: function handleEvent(aEvent) { |
|
503 switch (aEvent.type) { |
|
504 case "keypress": |
|
505 // this.commands is not holding focus and not processing key events. |
|
506 // Proxying events so that they're handled properly. |
|
507 |
|
508 // Avoid recursion |
|
509 if (aEvent.mine) |
|
510 break; |
|
511 |
|
512 let ev = document.createEvent("KeyboardEvent"); |
|
513 ev.initKeyEvent( |
|
514 "keypress", // in DOMString typeArg, |
|
515 false, // in boolean canBubbleArg, |
|
516 true, // in boolean cancelableArg, |
|
517 null, // in nsIDOMAbstractView viewArg, Specifies UIEvent.view. This value may be null. |
|
518 aEvent.ctrlKey, // in boolean ctrlKeyArg, |
|
519 aEvent.altKey, // in boolean altKeyArg, |
|
520 aEvent.shiftKey, // in boolean shiftKeyArg, |
|
521 aEvent.metaKey, // in boolean metaKeyArg, |
|
522 aEvent.keyCode, // in unsigned long keyCodeArg, |
|
523 aEvent.charCode); // in unsigned long charCodeArg); |
|
524 |
|
525 ev.mine = true; |
|
526 |
|
527 switch (aEvent.keyCode) { |
|
528 case aEvent.DOM_VK_ESCAPE: |
|
529 this.hide(); |
|
530 break; |
|
531 |
|
532 case aEvent.DOM_VK_RETURN: |
|
533 this.commands.currentItem.click(); |
|
534 break; |
|
535 } |
|
536 |
|
537 if (Util.isNavigationKey(aEvent.keyCode)) { |
|
538 aEvent.stopPropagation(); |
|
539 aEvent.preventDefault(); |
|
540 this.commands.dispatchEvent(ev); |
|
541 } else if (!this._wantTypeBehind) { |
|
542 // Hide the context menu so you can't type behind it. |
|
543 aEvent.stopPropagation(); |
|
544 aEvent.preventDefault(); |
|
545 this.hide(); |
|
546 } |
|
547 break; |
|
548 case "blur": |
|
549 case "mousedown": |
|
550 case "touchstart": |
|
551 case "scroll": |
|
552 if (!this._popup.contains(aEvent.target)) { |
|
553 aEvent.stopPropagation(); |
|
554 this.hide(); |
|
555 } |
|
556 break; |
|
557 case "PopupChanged": |
|
558 if (aEvent.detail) { |
|
559 this.hide(); |
|
560 } |
|
561 break; |
|
562 case "MozAppbarShowing": |
|
563 if (this.controller && this.controller.hide) { |
|
564 this.controller.hide() |
|
565 } else { |
|
566 this.hide(); |
|
567 } |
|
568 break; |
|
569 } |
|
570 } |
|
571 }; |