1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/metro/base/content/helperui/MenuUI.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,571 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +// Positioning buffer enforced between the edge of a context menu 1.9 +// and the edge of the screen. 1.10 +const kPositionPadding = 10; 1.11 + 1.12 +var AutofillMenuUI = { 1.13 + _popupState: null, 1.14 + __menuPopup: null, 1.15 + 1.16 + get _panel() { return document.getElementById("autofill-container"); }, 1.17 + get _popup() { return document.getElementById("autofill-popup"); }, 1.18 + get commands() { return this._popup.childNodes[0]; }, 1.19 + 1.20 + get _menuPopup() { 1.21 + if (!this.__menuPopup) { 1.22 + this.__menuPopup = new MenuPopup(this._panel, this._popup); 1.23 + this.__menuPopup._wantTypeBehind = true; 1.24 + this.__menuPopup.controller = this; 1.25 + } 1.26 + return this.__menuPopup; 1.27 + }, 1.28 + 1.29 + _firePopupEvent: function _firePopupEvent(aEventName) { 1.30 + let menupopup = this._currentControl.menupopup; 1.31 + if (menupopup.hasAttribute(aEventName)) { 1.32 + let func = new Function("event", menupopup.getAttribute(aEventName)); 1.33 + func.call(this); 1.34 + } 1.35 + }, 1.36 + 1.37 + _emptyCommands: function _emptyCommands() { 1.38 + while (this.commands.firstChild) 1.39 + this.commands.removeChild(this.commands.firstChild); 1.40 + }, 1.41 + 1.42 + _positionOptions: function _positionOptions() { 1.43 + return { 1.44 + bottomAligned: false, 1.45 + leftAligned: true, 1.46 + xPos: this._anchorRect.x, 1.47 + yPos: this._anchorRect.y + this._anchorRect.height, 1.48 + maxWidth: this._anchorRect.width, 1.49 + maxHeight: 350, 1.50 + source: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH 1.51 + }; 1.52 + }, 1.53 + 1.54 + show: function show(aAnchorRect, aSuggestionsList) { 1.55 + this.commands.addEventListener("select", this, true); 1.56 + 1.57 + this._anchorRect = aAnchorRect; 1.58 + this._emptyCommands(); 1.59 + for (let idx = 0; idx < aSuggestionsList.length; idx++) { 1.60 + let item = document.createElement("richlistitem"); 1.61 + let label = document.createElement("label"); 1.62 + label.setAttribute("value", aSuggestionsList[idx].label); 1.63 + item.setAttribute("value", aSuggestionsList[idx].value); 1.64 + item.setAttribute("data", aSuggestionsList[idx].value); 1.65 + item.appendChild(label); 1.66 + this.commands.appendChild(item); 1.67 + } 1.68 + this._menuPopup.show(this._positionOptions()); 1.69 + }, 1.70 + 1.71 + selectByIndex: function mn_selectByIndex(aIndex) { 1.72 + this._menuPopup.hide(); 1.73 + FormHelperUI.doAutoComplete(this.commands.childNodes[aIndex].getAttribute("data")); 1.74 + }, 1.75 + 1.76 + hide: function hide () { 1.77 + this.commands.removeEventListener("select", this, true); 1.78 + 1.79 + this._menuPopup.hide(); 1.80 + }, 1.81 + 1.82 + handleEvent: function (aEvent) { 1.83 + switch (aEvent.type) { 1.84 + case "select": 1.85 + FormHelperUI.doAutoComplete(this.commands.value); 1.86 + break; 1.87 + } 1.88 + } 1.89 +}; 1.90 + 1.91 +var ContextMenuUI = { 1.92 + _popupState: null, 1.93 + __menuPopup: null, 1.94 + _defaultPositionOptions: { 1.95 + bottomAligned: true, 1.96 + rightAligned: false, 1.97 + centerHorizontally: true, 1.98 + moveBelowToFit: true 1.99 + }, 1.100 + 1.101 + get _panel() { return document.getElementById("context-container"); }, 1.102 + get _popup() { return document.getElementById("context-popup"); }, 1.103 + get commands() { return this._popup.childNodes[0]; }, 1.104 + 1.105 + get _menuPopup() { 1.106 + if (!this.__menuPopup) { 1.107 + this.__menuPopup = new MenuPopup(this._panel, this._popup); 1.108 + this.__menuPopup.controller = this; 1.109 + } 1.110 + return this.__menuPopup; 1.111 + }, 1.112 + 1.113 + /******************************************* 1.114 + * External api 1.115 + */ 1.116 + 1.117 + /* 1.118 + * popupState - return the json object for this context. Called 1.119 + * by context command to invoke actions on the target. 1.120 + */ 1.121 + get popupState() { 1.122 + return this._popupState; 1.123 + }, 1.124 + 1.125 + /* 1.126 + * showContextMenu - display a context sensitive menu based 1.127 + * on the data provided in a json data structure. 1.128 + * 1.129 + * @param aMessage data structure containing information about 1.130 + * the context. 1.131 + * aMessage.json - json data structure described below. 1.132 + * aMessage.target - target element on which to evoke 1.133 + * 1.134 + * @returns true if the context menu was displayed, 1.135 + * false otherwise. 1.136 + * 1.137 + * json: TBD 1.138 + */ 1.139 + showContextMenu: function ch_showContextMenu(aMessage) { 1.140 + this._popupState = aMessage.json; 1.141 + this._popupState.target = aMessage.target; 1.142 + let contentTypes = this._popupState.types; 1.143 + 1.144 + /* 1.145 + * Types in ContextMenuHandler: 1.146 + * image 1.147 + * link 1.148 + * input-text - generic form input control 1.149 + * copy - form input that has some selected text 1.150 + * selectable - form input with text that can be selected 1.151 + * input-empty - form input (empty) 1.152 + * paste - form input and there's text on the clipboard 1.153 + * selected-text - generic content text that is selected 1.154 + * content-text - generic content text 1.155 + * video 1.156 + * media-paused, media-playing 1.157 + * paste-url - url bar w/text on the clipboard 1.158 + */ 1.159 + 1.160 + Util.dumpLn("contentTypes:", contentTypes); 1.161 + 1.162 + // Defines whether or not low priority items in images, text, and 1.163 + // links are displayed. 1.164 + let multipleMediaTypes = false; 1.165 + if (contentTypes.indexOf("link") != -1 && 1.166 + (contentTypes.indexOf("image") != -1 || 1.167 + contentTypes.indexOf("video") != -1 || 1.168 + contentTypes.indexOf("selected-text") != -1)) 1.169 + multipleMediaTypes = true; 1.170 + 1.171 + for (let command of Array.slice(this.commands.childNodes)) { 1.172 + command.hidden = true; 1.173 + command.selected = false; 1.174 + } 1.175 + 1.176 + let optionsAvailable = false; 1.177 + for (let command of Array.slice(this.commands.childNodes)) { 1.178 + let types = command.getAttribute("type").split(","); 1.179 + let lowPriority = (command.hasAttribute("priority") && 1.180 + command.getAttribute("priority") == "low"); 1.181 + let searchTextItem = (command.id == "context-search"); 1.182 + 1.183 + // filter low priority items if we have more than one media type. 1.184 + if (multipleMediaTypes && lowPriority) 1.185 + continue; 1.186 + 1.187 + for (let i = 0; i < types.length; i++) { 1.188 + // If one of the item's types has '!' before it, treat it as an exclusion rule. 1.189 + if (types[i].charAt(0) == '!' && contentTypes.indexOf(types[i].substring(1)) != -1) { 1.190 + break; 1.191 + } 1.192 + if (contentTypes.indexOf(types[i]) != -1) { 1.193 + // If this is the special search text item, we need to set its label dynamically. 1.194 + if (searchTextItem && !ContextCommands.searchTextSetup(command, this._popupState.string)) { 1.195 + break; 1.196 + } 1.197 + optionsAvailable = true; 1.198 + command.hidden = false; 1.199 + break; 1.200 + } 1.201 + } 1.202 + } 1.203 + 1.204 + if (!optionsAvailable) { 1.205 + this._popupState = null; 1.206 + return false; 1.207 + } 1.208 + 1.209 + let coords = { x: aMessage.json.xPos, y: aMessage.json.yPos }; 1.210 + 1.211 + // chrome calls don't need to be translated and as such 1.212 + // don't provide target. 1.213 + if (aMessage.target && aMessage.target.localName === "browser") { 1.214 + coords = aMessage.target.msgBrowserToClient(aMessage, true); 1.215 + } 1.216 + this._menuPopup.show(Util.extend({}, this._defaultPositionOptions, { 1.217 + xPos: coords.x, 1.218 + yPos: coords.y, 1.219 + source: aMessage.json.source 1.220 + })); 1.221 + return true; 1.222 + }, 1.223 + 1.224 + hide: function hide () { 1.225 + for (let command of this.commands.querySelectorAll("richlistitem[selected]")) { 1.226 + command.removeAttribute("selected"); 1.227 + } 1.228 + this._menuPopup.hide(); 1.229 + this._popupState = null; 1.230 + }, 1.231 + 1.232 + reset: function reset() { 1.233 + this._popupState = null; 1.234 + } 1.235 +}; 1.236 + 1.237 +var MenuControlUI = { 1.238 + _currentControl: null, 1.239 + __menuPopup: null, 1.240 + 1.241 + get _panel() { return document.getElementById("menucontrol-container"); }, 1.242 + get _popup() { return document.getElementById("menucontrol-popup"); }, 1.243 + get commands() { return this._popup.childNodes[0]; }, 1.244 + 1.245 + get _menuPopup() { 1.246 + if (!this.__menuPopup) { 1.247 + this.__menuPopup = new MenuPopup(this._panel, this._popup); 1.248 + this.__menuPopup.controller = this; 1.249 + } 1.250 + return this.__menuPopup; 1.251 + }, 1.252 + 1.253 + _firePopupEvent: function _firePopupEvent(aEventName) { 1.254 + let menupopup = this._currentControl.menupopup; 1.255 + if (menupopup.hasAttribute(aEventName)) { 1.256 + let func = new Function("event", menupopup.getAttribute(aEventName)); 1.257 + func.call(this); 1.258 + } 1.259 + }, 1.260 + 1.261 + _emptyCommands: function _emptyCommands() { 1.262 + while (this.commands.firstChild) 1.263 + this.commands.removeChild(this.commands.firstChild); 1.264 + }, 1.265 + 1.266 + _positionOptions: function _positionOptions() { 1.267 + let position = this._currentControl.menupopup.position || "after_start"; 1.268 + let rect = this._currentControl.getBoundingClientRect(); 1.269 + 1.270 + let options = {}; 1.271 + 1.272 + // TODO: Detect text direction and flip for RTL. 1.273 + 1.274 + switch (position) { 1.275 + case "before_start": 1.276 + options.xPos = rect.left; 1.277 + options.yPos = rect.top; 1.278 + options.bottomAligned = true; 1.279 + options.leftAligned = true; 1.280 + break; 1.281 + case "before_end": 1.282 + options.xPos = rect.right; 1.283 + options.yPos = rect.top; 1.284 + options.bottomAligned = true; 1.285 + options.rightAligned = true; 1.286 + break; 1.287 + case "after_start": 1.288 + options.xPos = rect.left; 1.289 + options.yPos = rect.bottom; 1.290 + options.topAligned = true; 1.291 + options.leftAligned = true; 1.292 + break; 1.293 + case "after_end": 1.294 + options.xPos = rect.right; 1.295 + options.yPos = rect.bottom; 1.296 + options.topAligned = true; 1.297 + options.rightAligned = true; 1.298 + break; 1.299 + 1.300 + // TODO: Support other popup positions. 1.301 + } 1.302 + 1.303 + return options; 1.304 + }, 1.305 + 1.306 + show: function show(aMenuControl) { 1.307 + this._currentControl = aMenuControl; 1.308 + this._panel.setAttribute("for", aMenuControl.id); 1.309 + this._firePopupEvent("onpopupshowing"); 1.310 + 1.311 + this._emptyCommands(); 1.312 + let children = this._currentControl.menupopup.children; 1.313 + for (let i = 0; i < children.length; i++) { 1.314 + let child = children[i]; 1.315 + let item = document.createElement("richlistitem"); 1.316 + 1.317 + if (child.disabled) 1.318 + item.setAttribute("disabled", "true"); 1.319 + 1.320 + if (child.hidden) 1.321 + item.setAttribute("hidden", "true"); 1.322 + 1.323 + // Add selected as a class name instead of an attribute to not being overidden 1.324 + // by the richlistbox behavior (it sets the "current" and "selected" attribute 1.325 + if (child.selected) 1.326 + item.setAttribute("class", "selected"); 1.327 + 1.328 + let image = document.createElement("image"); 1.329 + image.setAttribute("src", child.image || ""); 1.330 + item.appendChild(image); 1.331 + 1.332 + let label = document.createElement("label"); 1.333 + label.setAttribute("value", child.label); 1.334 + item.appendChild(label); 1.335 + 1.336 + this.commands.appendChild(item); 1.337 + } 1.338 + 1.339 + this._menuPopup.show(this._positionOptions()); 1.340 + }, 1.341 + 1.342 + selectByIndex: function mn_selectByIndex(aIndex) { 1.343 + this._currentControl.selectedIndex = aIndex; 1.344 + 1.345 + // Dispatch a xul command event to the attached menulist 1.346 + if (this._currentControl.dispatchEvent) { 1.347 + let evt = document.createEvent("XULCommandEvent"); 1.348 + evt.initCommandEvent("command", true, true, window, 0, false, false, false, false, null); 1.349 + this._currentControl.dispatchEvent(evt); 1.350 + } 1.351 + 1.352 + this._menuPopup.hide(); 1.353 + } 1.354 +}; 1.355 + 1.356 +function MenuPopup(aPanel, aPopup) { 1.357 + this._panel = aPanel; 1.358 + this._popup = aPopup; 1.359 + this._wantTypeBehind = false; 1.360 + 1.361 + window.addEventListener('MozAppbarShowing', this, false); 1.362 +} 1.363 +MenuPopup.prototype = { 1.364 + get visible() { return !this._panel.hidden; }, 1.365 + get commands() { return this._popup.childNodes[0]; }, 1.366 + 1.367 + show: function (aPositionOptions) { 1.368 + if (!this.visible) { 1.369 + this._animateShow(aPositionOptions); 1.370 + } 1.371 + }, 1.372 + 1.373 + hide: function () { 1.374 + if (this.visible) { 1.375 + this._animateHide(); 1.376 + } 1.377 + }, 1.378 + 1.379 + _position: function _position(aPositionOptions) { 1.380 + let aX = aPositionOptions.xPos; 1.381 + let aY = aPositionOptions.yPos; 1.382 + let aSource = aPositionOptions.source; 1.383 + 1.384 + // Set these first so they are set when we do misc. calculations below. 1.385 + if (aPositionOptions.maxWidth) { 1.386 + this._popup.style.maxWidth = aPositionOptions.maxWidth + "px"; 1.387 + } 1.388 + if (aPositionOptions.maxHeight) { 1.389 + this._popup.style.maxHeight = aPositionOptions.maxHeight + "px"; 1.390 + } 1.391 + 1.392 + let width = this._popup.boxObject.width; 1.393 + let height = this._popup.boxObject.height; 1.394 + let halfWidth = width / 2; 1.395 + let screenWidth = ContentAreaObserver.width; 1.396 + let screenHeight = ContentAreaObserver.height; 1.397 + 1.398 + if (aPositionOptions.rightAligned) 1.399 + aX -= width; 1.400 + 1.401 + if (aPositionOptions.bottomAligned) 1.402 + aY -= height; 1.403 + 1.404 + if (aPositionOptions.centerHorizontally) 1.405 + aX -= halfWidth; 1.406 + 1.407 + // Always leave some padding. 1.408 + if (aX < kPositionPadding) { 1.409 + aX = kPositionPadding; 1.410 + } else if (aX + width + kPositionPadding > screenWidth){ 1.411 + // Don't let the popup overflow to the right. 1.412 + aX = Math.max(screenWidth - width - kPositionPadding, kPositionPadding); 1.413 + } 1.414 + 1.415 + if (aY < kPositionPadding && aPositionOptions.moveBelowToFit) { 1.416 + // show context menu below when it doesn't fit. 1.417 + aY = aPositionOptions.yPos; 1.418 + } 1.419 + 1.420 + if (aY < kPositionPadding) { 1.421 + aY = kPositionPadding; 1.422 + } else if (aY + height + kPositionPadding > screenHeight){ 1.423 + aY = Math.max(screenHeight - height - kPositionPadding, kPositionPadding); 1.424 + } 1.425 + 1.426 + this._panel.left = aX; 1.427 + this._panel.top = aY; 1.428 + 1.429 + if (!aPositionOptions.maxHeight) { 1.430 + // Make sure it fits in the window. 1.431 + let popupHeight = Math.min(aY + height + kPositionPadding, screenHeight - aY - kPositionPadding); 1.432 + this._popup.style.maxHeight = popupHeight + "px"; 1.433 + } 1.434 + 1.435 + if (!aPositionOptions.maxWidth) { 1.436 + let popupWidth = Math.min(aX + width + kPositionPadding, screenWidth - aX - kPositionPadding); 1.437 + this._popup.style.maxWidth = popupWidth + "px"; 1.438 + } 1.439 + }, 1.440 + 1.441 + _animateShow: function (aPositionOptions) { 1.442 + let deferred = Promise.defer(); 1.443 + 1.444 + window.addEventListener("keypress", this, true); 1.445 + window.addEventListener("mousedown", this, true); 1.446 + window.addEventListener("touchstart", this, true); 1.447 + window.addEventListener("scroll", this, true); 1.448 + window.addEventListener("blur", this, true); 1.449 + Elements.stack.addEventListener("PopupChanged", this, false); 1.450 + 1.451 + this._panel.hidden = false; 1.452 + let popupFrom = !aPositionOptions.bottomAligned ? "above" : "below"; 1.453 + this._panel.setAttribute("showingfrom", popupFrom); 1.454 + 1.455 + // This triggers a reflow, which sets transitionability. 1.456 + // All animation/transition setup must happen before here. 1.457 + this._position(aPositionOptions || {}); 1.458 + 1.459 + let self = this; 1.460 + this._panel.addEventListener("transitionend", function popupshown () { 1.461 + self._panel.removeEventListener("transitionend", popupshown); 1.462 + self._panel.removeAttribute("showingfrom"); 1.463 + 1.464 + self._dispatch("popupshown"); 1.465 + deferred.resolve(); 1.466 + }); 1.467 + 1.468 + this._panel.setAttribute("showing", "true"); 1.469 + return deferred.promise; 1.470 + }, 1.471 + 1.472 + _animateHide: function () { 1.473 + let deferred = Promise.defer(); 1.474 + 1.475 + window.removeEventListener("keypress", this, true); 1.476 + window.removeEventListener("mousedown", this, true); 1.477 + window.removeEventListener("touchstart", this, true); 1.478 + window.removeEventListener("scroll", this, true); 1.479 + window.removeEventListener("blur", this, true); 1.480 + Elements.stack.removeEventListener("PopupChanged", this, false); 1.481 + 1.482 + let self = this; 1.483 + this._panel.addEventListener("transitionend", function popuphidden() { 1.484 + self._panel.removeEventListener("transitionend", popuphidden); 1.485 + self._panel.removeAttribute("hiding"); 1.486 + self._panel.hidden = true; 1.487 + self._popup.style.maxWidth = "none"; 1.488 + self._popup.style.maxHeight = "none"; 1.489 + 1.490 + self._dispatch("popuphidden"); 1.491 + deferred.resolve(); 1.492 + }); 1.493 + 1.494 + this._panel.setAttribute("hiding", "true"); 1.495 + this._panel.removeAttribute("showing"); 1.496 + return deferred.promise; 1.497 + }, 1.498 + 1.499 + _dispatch: function _dispatch(aName) { 1.500 + let event = document.createEvent("Events"); 1.501 + event.initEvent(aName, true, false); 1.502 + this._panel.dispatchEvent(event); 1.503 + }, 1.504 + 1.505 + handleEvent: function handleEvent(aEvent) { 1.506 + switch (aEvent.type) { 1.507 + case "keypress": 1.508 + // this.commands is not holding focus and not processing key events. 1.509 + // Proxying events so that they're handled properly. 1.510 + 1.511 + // Avoid recursion 1.512 + if (aEvent.mine) 1.513 + break; 1.514 + 1.515 + let ev = document.createEvent("KeyboardEvent"); 1.516 + ev.initKeyEvent( 1.517 + "keypress", // in DOMString typeArg, 1.518 + false, // in boolean canBubbleArg, 1.519 + true, // in boolean cancelableArg, 1.520 + null, // in nsIDOMAbstractView viewArg, Specifies UIEvent.view. This value may be null. 1.521 + aEvent.ctrlKey, // in boolean ctrlKeyArg, 1.522 + aEvent.altKey, // in boolean altKeyArg, 1.523 + aEvent.shiftKey, // in boolean shiftKeyArg, 1.524 + aEvent.metaKey, // in boolean metaKeyArg, 1.525 + aEvent.keyCode, // in unsigned long keyCodeArg, 1.526 + aEvent.charCode); // in unsigned long charCodeArg); 1.527 + 1.528 + ev.mine = true; 1.529 + 1.530 + switch (aEvent.keyCode) { 1.531 + case aEvent.DOM_VK_ESCAPE: 1.532 + this.hide(); 1.533 + break; 1.534 + 1.535 + case aEvent.DOM_VK_RETURN: 1.536 + this.commands.currentItem.click(); 1.537 + break; 1.538 + } 1.539 + 1.540 + if (Util.isNavigationKey(aEvent.keyCode)) { 1.541 + aEvent.stopPropagation(); 1.542 + aEvent.preventDefault(); 1.543 + this.commands.dispatchEvent(ev); 1.544 + } else if (!this._wantTypeBehind) { 1.545 + // Hide the context menu so you can't type behind it. 1.546 + aEvent.stopPropagation(); 1.547 + aEvent.preventDefault(); 1.548 + this.hide(); 1.549 + } 1.550 + break; 1.551 + case "blur": 1.552 + case "mousedown": 1.553 + case "touchstart": 1.554 + case "scroll": 1.555 + if (!this._popup.contains(aEvent.target)) { 1.556 + aEvent.stopPropagation(); 1.557 + this.hide(); 1.558 + } 1.559 + break; 1.560 + case "PopupChanged": 1.561 + if (aEvent.detail) { 1.562 + this.hide(); 1.563 + } 1.564 + break; 1.565 + case "MozAppbarShowing": 1.566 + if (this.controller && this.controller.hide) { 1.567 + this.controller.hide() 1.568 + } else { 1.569 + this.hide(); 1.570 + } 1.571 + break; 1.572 + } 1.573 + } 1.574 +};