michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: // Positioning buffer enforced between the edge of a context menu michael@0: // and the edge of the screen. michael@0: const kPositionPadding = 10; michael@0: michael@0: var AutofillMenuUI = { michael@0: _popupState: null, michael@0: __menuPopup: null, michael@0: michael@0: get _panel() { return document.getElementById("autofill-container"); }, michael@0: get _popup() { return document.getElementById("autofill-popup"); }, michael@0: get commands() { return this._popup.childNodes[0]; }, michael@0: michael@0: get _menuPopup() { michael@0: if (!this.__menuPopup) { michael@0: this.__menuPopup = new MenuPopup(this._panel, this._popup); michael@0: this.__menuPopup._wantTypeBehind = true; michael@0: this.__menuPopup.controller = this; michael@0: } michael@0: return this.__menuPopup; michael@0: }, michael@0: michael@0: _firePopupEvent: function _firePopupEvent(aEventName) { michael@0: let menupopup = this._currentControl.menupopup; michael@0: if (menupopup.hasAttribute(aEventName)) { michael@0: let func = new Function("event", menupopup.getAttribute(aEventName)); michael@0: func.call(this); michael@0: } michael@0: }, michael@0: michael@0: _emptyCommands: function _emptyCommands() { michael@0: while (this.commands.firstChild) michael@0: this.commands.removeChild(this.commands.firstChild); michael@0: }, michael@0: michael@0: _positionOptions: function _positionOptions() { michael@0: return { michael@0: bottomAligned: false, michael@0: leftAligned: true, michael@0: xPos: this._anchorRect.x, michael@0: yPos: this._anchorRect.y + this._anchorRect.height, michael@0: maxWidth: this._anchorRect.width, michael@0: maxHeight: 350, michael@0: source: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH michael@0: }; michael@0: }, michael@0: michael@0: show: function show(aAnchorRect, aSuggestionsList) { michael@0: this.commands.addEventListener("select", this, true); michael@0: michael@0: this._anchorRect = aAnchorRect; michael@0: this._emptyCommands(); michael@0: for (let idx = 0; idx < aSuggestionsList.length; idx++) { michael@0: let item = document.createElement("richlistitem"); michael@0: let label = document.createElement("label"); michael@0: label.setAttribute("value", aSuggestionsList[idx].label); michael@0: item.setAttribute("value", aSuggestionsList[idx].value); michael@0: item.setAttribute("data", aSuggestionsList[idx].value); michael@0: item.appendChild(label); michael@0: this.commands.appendChild(item); michael@0: } michael@0: this._menuPopup.show(this._positionOptions()); michael@0: }, michael@0: michael@0: selectByIndex: function mn_selectByIndex(aIndex) { michael@0: this._menuPopup.hide(); michael@0: FormHelperUI.doAutoComplete(this.commands.childNodes[aIndex].getAttribute("data")); michael@0: }, michael@0: michael@0: hide: function hide () { michael@0: this.commands.removeEventListener("select", this, true); michael@0: michael@0: this._menuPopup.hide(); michael@0: }, michael@0: michael@0: handleEvent: function (aEvent) { michael@0: switch (aEvent.type) { michael@0: case "select": michael@0: FormHelperUI.doAutoComplete(this.commands.value); michael@0: break; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: var ContextMenuUI = { michael@0: _popupState: null, michael@0: __menuPopup: null, michael@0: _defaultPositionOptions: { michael@0: bottomAligned: true, michael@0: rightAligned: false, michael@0: centerHorizontally: true, michael@0: moveBelowToFit: true michael@0: }, michael@0: michael@0: get _panel() { return document.getElementById("context-container"); }, michael@0: get _popup() { return document.getElementById("context-popup"); }, michael@0: get commands() { return this._popup.childNodes[0]; }, michael@0: michael@0: get _menuPopup() { michael@0: if (!this.__menuPopup) { michael@0: this.__menuPopup = new MenuPopup(this._panel, this._popup); michael@0: this.__menuPopup.controller = this; michael@0: } michael@0: return this.__menuPopup; michael@0: }, michael@0: michael@0: /******************************************* michael@0: * External api michael@0: */ michael@0: michael@0: /* michael@0: * popupState - return the json object for this context. Called michael@0: * by context command to invoke actions on the target. michael@0: */ michael@0: get popupState() { michael@0: return this._popupState; michael@0: }, michael@0: michael@0: /* michael@0: * showContextMenu - display a context sensitive menu based michael@0: * on the data provided in a json data structure. michael@0: * michael@0: * @param aMessage data structure containing information about michael@0: * the context. michael@0: * aMessage.json - json data structure described below. michael@0: * aMessage.target - target element on which to evoke michael@0: * michael@0: * @returns true if the context menu was displayed, michael@0: * false otherwise. michael@0: * michael@0: * json: TBD michael@0: */ michael@0: showContextMenu: function ch_showContextMenu(aMessage) { michael@0: this._popupState = aMessage.json; michael@0: this._popupState.target = aMessage.target; michael@0: let contentTypes = this._popupState.types; michael@0: michael@0: /* michael@0: * Types in ContextMenuHandler: michael@0: * image michael@0: * link michael@0: * input-text - generic form input control michael@0: * copy - form input that has some selected text michael@0: * selectable - form input with text that can be selected michael@0: * input-empty - form input (empty) michael@0: * paste - form input and there's text on the clipboard michael@0: * selected-text - generic content text that is selected michael@0: * content-text - generic content text michael@0: * video michael@0: * media-paused, media-playing michael@0: * paste-url - url bar w/text on the clipboard michael@0: */ michael@0: michael@0: Util.dumpLn("contentTypes:", contentTypes); michael@0: michael@0: // Defines whether or not low priority items in images, text, and michael@0: // links are displayed. michael@0: let multipleMediaTypes = false; michael@0: if (contentTypes.indexOf("link") != -1 && michael@0: (contentTypes.indexOf("image") != -1 || michael@0: contentTypes.indexOf("video") != -1 || michael@0: contentTypes.indexOf("selected-text") != -1)) michael@0: multipleMediaTypes = true; michael@0: michael@0: for (let command of Array.slice(this.commands.childNodes)) { michael@0: command.hidden = true; michael@0: command.selected = false; michael@0: } michael@0: michael@0: let optionsAvailable = false; michael@0: for (let command of Array.slice(this.commands.childNodes)) { michael@0: let types = command.getAttribute("type").split(","); michael@0: let lowPriority = (command.hasAttribute("priority") && michael@0: command.getAttribute("priority") == "low"); michael@0: let searchTextItem = (command.id == "context-search"); michael@0: michael@0: // filter low priority items if we have more than one media type. michael@0: if (multipleMediaTypes && lowPriority) michael@0: continue; michael@0: michael@0: for (let i = 0; i < types.length; i++) { michael@0: // If one of the item's types has '!' before it, treat it as an exclusion rule. michael@0: if (types[i].charAt(0) == '!' && contentTypes.indexOf(types[i].substring(1)) != -1) { michael@0: break; michael@0: } michael@0: if (contentTypes.indexOf(types[i]) != -1) { michael@0: // If this is the special search text item, we need to set its label dynamically. michael@0: if (searchTextItem && !ContextCommands.searchTextSetup(command, this._popupState.string)) { michael@0: break; michael@0: } michael@0: optionsAvailable = true; michael@0: command.hidden = false; michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (!optionsAvailable) { michael@0: this._popupState = null; michael@0: return false; michael@0: } michael@0: michael@0: let coords = { x: aMessage.json.xPos, y: aMessage.json.yPos }; michael@0: michael@0: // chrome calls don't need to be translated and as such michael@0: // don't provide target. michael@0: if (aMessage.target && aMessage.target.localName === "browser") { michael@0: coords = aMessage.target.msgBrowserToClient(aMessage, true); michael@0: } michael@0: this._menuPopup.show(Util.extend({}, this._defaultPositionOptions, { michael@0: xPos: coords.x, michael@0: yPos: coords.y, michael@0: source: aMessage.json.source michael@0: })); michael@0: return true; michael@0: }, michael@0: michael@0: hide: function hide () { michael@0: for (let command of this.commands.querySelectorAll("richlistitem[selected]")) { michael@0: command.removeAttribute("selected"); michael@0: } michael@0: this._menuPopup.hide(); michael@0: this._popupState = null; michael@0: }, michael@0: michael@0: reset: function reset() { michael@0: this._popupState = null; michael@0: } michael@0: }; michael@0: michael@0: var MenuControlUI = { michael@0: _currentControl: null, michael@0: __menuPopup: null, michael@0: michael@0: get _panel() { return document.getElementById("menucontrol-container"); }, michael@0: get _popup() { return document.getElementById("menucontrol-popup"); }, michael@0: get commands() { return this._popup.childNodes[0]; }, michael@0: michael@0: get _menuPopup() { michael@0: if (!this.__menuPopup) { michael@0: this.__menuPopup = new MenuPopup(this._panel, this._popup); michael@0: this.__menuPopup.controller = this; michael@0: } michael@0: return this.__menuPopup; michael@0: }, michael@0: michael@0: _firePopupEvent: function _firePopupEvent(aEventName) { michael@0: let menupopup = this._currentControl.menupopup; michael@0: if (menupopup.hasAttribute(aEventName)) { michael@0: let func = new Function("event", menupopup.getAttribute(aEventName)); michael@0: func.call(this); michael@0: } michael@0: }, michael@0: michael@0: _emptyCommands: function _emptyCommands() { michael@0: while (this.commands.firstChild) michael@0: this.commands.removeChild(this.commands.firstChild); michael@0: }, michael@0: michael@0: _positionOptions: function _positionOptions() { michael@0: let position = this._currentControl.menupopup.position || "after_start"; michael@0: let rect = this._currentControl.getBoundingClientRect(); michael@0: michael@0: let options = {}; michael@0: michael@0: // TODO: Detect text direction and flip for RTL. michael@0: michael@0: switch (position) { michael@0: case "before_start": michael@0: options.xPos = rect.left; michael@0: options.yPos = rect.top; michael@0: options.bottomAligned = true; michael@0: options.leftAligned = true; michael@0: break; michael@0: case "before_end": michael@0: options.xPos = rect.right; michael@0: options.yPos = rect.top; michael@0: options.bottomAligned = true; michael@0: options.rightAligned = true; michael@0: break; michael@0: case "after_start": michael@0: options.xPos = rect.left; michael@0: options.yPos = rect.bottom; michael@0: options.topAligned = true; michael@0: options.leftAligned = true; michael@0: break; michael@0: case "after_end": michael@0: options.xPos = rect.right; michael@0: options.yPos = rect.bottom; michael@0: options.topAligned = true; michael@0: options.rightAligned = true; michael@0: break; michael@0: michael@0: // TODO: Support other popup positions. michael@0: } michael@0: michael@0: return options; michael@0: }, michael@0: michael@0: show: function show(aMenuControl) { michael@0: this._currentControl = aMenuControl; michael@0: this._panel.setAttribute("for", aMenuControl.id); michael@0: this._firePopupEvent("onpopupshowing"); michael@0: michael@0: this._emptyCommands(); michael@0: let children = this._currentControl.menupopup.children; michael@0: for (let i = 0; i < children.length; i++) { michael@0: let child = children[i]; michael@0: let item = document.createElement("richlistitem"); michael@0: michael@0: if (child.disabled) michael@0: item.setAttribute("disabled", "true"); michael@0: michael@0: if (child.hidden) michael@0: item.setAttribute("hidden", "true"); michael@0: michael@0: // Add selected as a class name instead of an attribute to not being overidden michael@0: // by the richlistbox behavior (it sets the "current" and "selected" attribute michael@0: if (child.selected) michael@0: item.setAttribute("class", "selected"); michael@0: michael@0: let image = document.createElement("image"); michael@0: image.setAttribute("src", child.image || ""); michael@0: item.appendChild(image); michael@0: michael@0: let label = document.createElement("label"); michael@0: label.setAttribute("value", child.label); michael@0: item.appendChild(label); michael@0: michael@0: this.commands.appendChild(item); michael@0: } michael@0: michael@0: this._menuPopup.show(this._positionOptions()); michael@0: }, michael@0: michael@0: selectByIndex: function mn_selectByIndex(aIndex) { michael@0: this._currentControl.selectedIndex = aIndex; michael@0: michael@0: // Dispatch a xul command event to the attached menulist michael@0: if (this._currentControl.dispatchEvent) { michael@0: let evt = document.createEvent("XULCommandEvent"); michael@0: evt.initCommandEvent("command", true, true, window, 0, false, false, false, false, null); michael@0: this._currentControl.dispatchEvent(evt); michael@0: } michael@0: michael@0: this._menuPopup.hide(); michael@0: } michael@0: }; michael@0: michael@0: function MenuPopup(aPanel, aPopup) { michael@0: this._panel = aPanel; michael@0: this._popup = aPopup; michael@0: this._wantTypeBehind = false; michael@0: michael@0: window.addEventListener('MozAppbarShowing', this, false); michael@0: } michael@0: MenuPopup.prototype = { michael@0: get visible() { return !this._panel.hidden; }, michael@0: get commands() { return this._popup.childNodes[0]; }, michael@0: michael@0: show: function (aPositionOptions) { michael@0: if (!this.visible) { michael@0: this._animateShow(aPositionOptions); michael@0: } michael@0: }, michael@0: michael@0: hide: function () { michael@0: if (this.visible) { michael@0: this._animateHide(); michael@0: } michael@0: }, michael@0: michael@0: _position: function _position(aPositionOptions) { michael@0: let aX = aPositionOptions.xPos; michael@0: let aY = aPositionOptions.yPos; michael@0: let aSource = aPositionOptions.source; michael@0: michael@0: // Set these first so they are set when we do misc. calculations below. michael@0: if (aPositionOptions.maxWidth) { michael@0: this._popup.style.maxWidth = aPositionOptions.maxWidth + "px"; michael@0: } michael@0: if (aPositionOptions.maxHeight) { michael@0: this._popup.style.maxHeight = aPositionOptions.maxHeight + "px"; michael@0: } michael@0: michael@0: let width = this._popup.boxObject.width; michael@0: let height = this._popup.boxObject.height; michael@0: let halfWidth = width / 2; michael@0: let screenWidth = ContentAreaObserver.width; michael@0: let screenHeight = ContentAreaObserver.height; michael@0: michael@0: if (aPositionOptions.rightAligned) michael@0: aX -= width; michael@0: michael@0: if (aPositionOptions.bottomAligned) michael@0: aY -= height; michael@0: michael@0: if (aPositionOptions.centerHorizontally) michael@0: aX -= halfWidth; michael@0: michael@0: // Always leave some padding. michael@0: if (aX < kPositionPadding) { michael@0: aX = kPositionPadding; michael@0: } else if (aX + width + kPositionPadding > screenWidth){ michael@0: // Don't let the popup overflow to the right. michael@0: aX = Math.max(screenWidth - width - kPositionPadding, kPositionPadding); michael@0: } michael@0: michael@0: if (aY < kPositionPadding && aPositionOptions.moveBelowToFit) { michael@0: // show context menu below when it doesn't fit. michael@0: aY = aPositionOptions.yPos; michael@0: } michael@0: michael@0: if (aY < kPositionPadding) { michael@0: aY = kPositionPadding; michael@0: } else if (aY + height + kPositionPadding > screenHeight){ michael@0: aY = Math.max(screenHeight - height - kPositionPadding, kPositionPadding); michael@0: } michael@0: michael@0: this._panel.left = aX; michael@0: this._panel.top = aY; michael@0: michael@0: if (!aPositionOptions.maxHeight) { michael@0: // Make sure it fits in the window. michael@0: let popupHeight = Math.min(aY + height + kPositionPadding, screenHeight - aY - kPositionPadding); michael@0: this._popup.style.maxHeight = popupHeight + "px"; michael@0: } michael@0: michael@0: if (!aPositionOptions.maxWidth) { michael@0: let popupWidth = Math.min(aX + width + kPositionPadding, screenWidth - aX - kPositionPadding); michael@0: this._popup.style.maxWidth = popupWidth + "px"; michael@0: } michael@0: }, michael@0: michael@0: _animateShow: function (aPositionOptions) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: window.addEventListener("keypress", this, true); michael@0: window.addEventListener("mousedown", this, true); michael@0: window.addEventListener("touchstart", this, true); michael@0: window.addEventListener("scroll", this, true); michael@0: window.addEventListener("blur", this, true); michael@0: Elements.stack.addEventListener("PopupChanged", this, false); michael@0: michael@0: this._panel.hidden = false; michael@0: let popupFrom = !aPositionOptions.bottomAligned ? "above" : "below"; michael@0: this._panel.setAttribute("showingfrom", popupFrom); michael@0: michael@0: // This triggers a reflow, which sets transitionability. michael@0: // All animation/transition setup must happen before here. michael@0: this._position(aPositionOptions || {}); michael@0: michael@0: let self = this; michael@0: this._panel.addEventListener("transitionend", function popupshown () { michael@0: self._panel.removeEventListener("transitionend", popupshown); michael@0: self._panel.removeAttribute("showingfrom"); michael@0: michael@0: self._dispatch("popupshown"); michael@0: deferred.resolve(); michael@0: }); michael@0: michael@0: this._panel.setAttribute("showing", "true"); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: _animateHide: function () { michael@0: let deferred = Promise.defer(); michael@0: michael@0: window.removeEventListener("keypress", this, true); michael@0: window.removeEventListener("mousedown", this, true); michael@0: window.removeEventListener("touchstart", this, true); michael@0: window.removeEventListener("scroll", this, true); michael@0: window.removeEventListener("blur", this, true); michael@0: Elements.stack.removeEventListener("PopupChanged", this, false); michael@0: michael@0: let self = this; michael@0: this._panel.addEventListener("transitionend", function popuphidden() { michael@0: self._panel.removeEventListener("transitionend", popuphidden); michael@0: self._panel.removeAttribute("hiding"); michael@0: self._panel.hidden = true; michael@0: self._popup.style.maxWidth = "none"; michael@0: self._popup.style.maxHeight = "none"; michael@0: michael@0: self._dispatch("popuphidden"); michael@0: deferred.resolve(); michael@0: }); michael@0: michael@0: this._panel.setAttribute("hiding", "true"); michael@0: this._panel.removeAttribute("showing"); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: _dispatch: function _dispatch(aName) { michael@0: let event = document.createEvent("Events"); michael@0: event.initEvent(aName, true, false); michael@0: this._panel.dispatchEvent(event); michael@0: }, michael@0: michael@0: handleEvent: function handleEvent(aEvent) { michael@0: switch (aEvent.type) { michael@0: case "keypress": michael@0: // this.commands is not holding focus and not processing key events. michael@0: // Proxying events so that they're handled properly. michael@0: michael@0: // Avoid recursion michael@0: if (aEvent.mine) michael@0: break; michael@0: michael@0: let ev = document.createEvent("KeyboardEvent"); michael@0: ev.initKeyEvent( michael@0: "keypress", // in DOMString typeArg, michael@0: false, // in boolean canBubbleArg, michael@0: true, // in boolean cancelableArg, michael@0: null, // in nsIDOMAbstractView viewArg, Specifies UIEvent.view. This value may be null. michael@0: aEvent.ctrlKey, // in boolean ctrlKeyArg, michael@0: aEvent.altKey, // in boolean altKeyArg, michael@0: aEvent.shiftKey, // in boolean shiftKeyArg, michael@0: aEvent.metaKey, // in boolean metaKeyArg, michael@0: aEvent.keyCode, // in unsigned long keyCodeArg, michael@0: aEvent.charCode); // in unsigned long charCodeArg); michael@0: michael@0: ev.mine = true; michael@0: michael@0: switch (aEvent.keyCode) { michael@0: case aEvent.DOM_VK_ESCAPE: michael@0: this.hide(); michael@0: break; michael@0: michael@0: case aEvent.DOM_VK_RETURN: michael@0: this.commands.currentItem.click(); michael@0: break; michael@0: } michael@0: michael@0: if (Util.isNavigationKey(aEvent.keyCode)) { michael@0: aEvent.stopPropagation(); michael@0: aEvent.preventDefault(); michael@0: this.commands.dispatchEvent(ev); michael@0: } else if (!this._wantTypeBehind) { michael@0: // Hide the context menu so you can't type behind it. michael@0: aEvent.stopPropagation(); michael@0: aEvent.preventDefault(); michael@0: this.hide(); michael@0: } michael@0: break; michael@0: case "blur": michael@0: case "mousedown": michael@0: case "touchstart": michael@0: case "scroll": michael@0: if (!this._popup.contains(aEvent.target)) { michael@0: aEvent.stopPropagation(); michael@0: this.hide(); michael@0: } michael@0: break; michael@0: case "PopupChanged": michael@0: if (aEvent.detail) { michael@0: this.hide(); michael@0: } michael@0: break; michael@0: case "MozAppbarShowing": michael@0: if (this.controller && this.controller.hide) { michael@0: this.controller.hide() michael@0: } else { michael@0: this.hide(); michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: };