Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
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/. */
5 // Positioning buffer enforced between the edge of a context menu
6 // and the edge of the screen.
7 const kPositionPadding = 10;
9 var AutofillMenuUI = {
10 _popupState: null,
11 __menuPopup: null,
13 get _panel() { return document.getElementById("autofill-container"); },
14 get _popup() { return document.getElementById("autofill-popup"); },
15 get commands() { return this._popup.childNodes[0]; },
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 },
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 },
34 _emptyCommands: function _emptyCommands() {
35 while (this.commands.firstChild)
36 this.commands.removeChild(this.commands.firstChild);
37 },
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 },
51 show: function show(aAnchorRect, aSuggestionsList) {
52 this.commands.addEventListener("select", this, true);
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 },
68 selectByIndex: function mn_selectByIndex(aIndex) {
69 this._menuPopup.hide();
70 FormHelperUI.doAutoComplete(this.commands.childNodes[aIndex].getAttribute("data"));
71 },
73 hide: function hide () {
74 this.commands.removeEventListener("select", this, true);
76 this._menuPopup.hide();
77 },
79 handleEvent: function (aEvent) {
80 switch (aEvent.type) {
81 case "select":
82 FormHelperUI.doAutoComplete(this.commands.value);
83 break;
84 }
85 }
86 };
88 var ContextMenuUI = {
89 _popupState: null,
90 __menuPopup: null,
91 _defaultPositionOptions: {
92 bottomAligned: true,
93 rightAligned: false,
94 centerHorizontally: true,
95 moveBelowToFit: true
96 },
98 get _panel() { return document.getElementById("context-container"); },
99 get _popup() { return document.getElementById("context-popup"); },
100 get commands() { return this._popup.childNodes[0]; },
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 },
110 /*******************************************
111 * External api
112 */
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 },
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;
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 */
157 Util.dumpLn("contentTypes:", contentTypes);
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;
168 for (let command of Array.slice(this.commands.childNodes)) {
169 command.hidden = true;
170 command.selected = false;
171 }
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");
180 // filter low priority items if we have more than one media type.
181 if (multipleMediaTypes && lowPriority)
182 continue;
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 }
201 if (!optionsAvailable) {
202 this._popupState = null;
203 return false;
204 }
206 let coords = { x: aMessage.json.xPos, y: aMessage.json.yPos };
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 },
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 },
229 reset: function reset() {
230 this._popupState = null;
231 }
232 };
234 var MenuControlUI = {
235 _currentControl: null,
236 __menuPopup: null,
238 get _panel() { return document.getElementById("menucontrol-container"); },
239 get _popup() { return document.getElementById("menucontrol-popup"); },
240 get commands() { return this._popup.childNodes[0]; },
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 },
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 },
258 _emptyCommands: function _emptyCommands() {
259 while (this.commands.firstChild)
260 this.commands.removeChild(this.commands.firstChild);
261 },
263 _positionOptions: function _positionOptions() {
264 let position = this._currentControl.menupopup.position || "after_start";
265 let rect = this._currentControl.getBoundingClientRect();
267 let options = {};
269 // TODO: Detect text direction and flip for RTL.
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;
297 // TODO: Support other popup positions.
298 }
300 return options;
301 },
303 show: function show(aMenuControl) {
304 this._currentControl = aMenuControl;
305 this._panel.setAttribute("for", aMenuControl.id);
306 this._firePopupEvent("onpopupshowing");
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");
314 if (child.disabled)
315 item.setAttribute("disabled", "true");
317 if (child.hidden)
318 item.setAttribute("hidden", "true");
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");
325 let image = document.createElement("image");
326 image.setAttribute("src", child.image || "");
327 item.appendChild(image);
329 let label = document.createElement("label");
330 label.setAttribute("value", child.label);
331 item.appendChild(label);
333 this.commands.appendChild(item);
334 }
336 this._menuPopup.show(this._positionOptions());
337 },
339 selectByIndex: function mn_selectByIndex(aIndex) {
340 this._currentControl.selectedIndex = aIndex;
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 }
349 this._menuPopup.hide();
350 }
351 };
353 function MenuPopup(aPanel, aPopup) {
354 this._panel = aPanel;
355 this._popup = aPopup;
356 this._wantTypeBehind = false;
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]; },
364 show: function (aPositionOptions) {
365 if (!this.visible) {
366 this._animateShow(aPositionOptions);
367 }
368 },
370 hide: function () {
371 if (this.visible) {
372 this._animateHide();
373 }
374 },
376 _position: function _position(aPositionOptions) {
377 let aX = aPositionOptions.xPos;
378 let aY = aPositionOptions.yPos;
379 let aSource = aPositionOptions.source;
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 }
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;
395 if (aPositionOptions.rightAligned)
396 aX -= width;
398 if (aPositionOptions.bottomAligned)
399 aY -= height;
401 if (aPositionOptions.centerHorizontally)
402 aX -= halfWidth;
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 }
412 if (aY < kPositionPadding && aPositionOptions.moveBelowToFit) {
413 // show context menu below when it doesn't fit.
414 aY = aPositionOptions.yPos;
415 }
417 if (aY < kPositionPadding) {
418 aY = kPositionPadding;
419 } else if (aY + height + kPositionPadding > screenHeight){
420 aY = Math.max(screenHeight - height - kPositionPadding, kPositionPadding);
421 }
423 this._panel.left = aX;
424 this._panel.top = aY;
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 }
432 if (!aPositionOptions.maxWidth) {
433 let popupWidth = Math.min(aX + width + kPositionPadding, screenWidth - aX - kPositionPadding);
434 this._popup.style.maxWidth = popupWidth + "px";
435 }
436 },
438 _animateShow: function (aPositionOptions) {
439 let deferred = Promise.defer();
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);
448 this._panel.hidden = false;
449 let popupFrom = !aPositionOptions.bottomAligned ? "above" : "below";
450 this._panel.setAttribute("showingfrom", popupFrom);
452 // This triggers a reflow, which sets transitionability.
453 // All animation/transition setup must happen before here.
454 this._position(aPositionOptions || {});
456 let self = this;
457 this._panel.addEventListener("transitionend", function popupshown () {
458 self._panel.removeEventListener("transitionend", popupshown);
459 self._panel.removeAttribute("showingfrom");
461 self._dispatch("popupshown");
462 deferred.resolve();
463 });
465 this._panel.setAttribute("showing", "true");
466 return deferred.promise;
467 },
469 _animateHide: function () {
470 let deferred = Promise.defer();
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);
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";
487 self._dispatch("popuphidden");
488 deferred.resolve();
489 });
491 this._panel.setAttribute("hiding", "true");
492 this._panel.removeAttribute("showing");
493 return deferred.promise;
494 },
496 _dispatch: function _dispatch(aName) {
497 let event = document.createEvent("Events");
498 event.initEvent(aName, true, false);
499 this._panel.dispatchEvent(event);
500 },
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.
508 // Avoid recursion
509 if (aEvent.mine)
510 break;
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);
525 ev.mine = true;
527 switch (aEvent.keyCode) {
528 case aEvent.DOM_VK_ESCAPE:
529 this.hide();
530 break;
532 case aEvent.DOM_VK_RETURN:
533 this.commands.currentItem.click();
534 break;
535 }
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 };