diff options
author | wolfbeast <mcwerewolf@gmail.com> | 2018-06-04 15:50:03 +0200 |
---|---|---|
committer | wolfbeast <mcwerewolf@gmail.com> | 2018-06-04 15:50:03 +0200 |
commit | e3b7744bee37c3d4a026d2193bed5e9439c40ff3 (patch) | |
tree | f3f7b07ca9bd78bf7ac2d76dd55b61b2a8bb549e /application/basilisk/base/content/urlbarBindings.xml | |
parent | cbce4f0b6a337f8250b62cae028f1c6d4cce51df (diff) | |
parent | 031afcafe288bf0f46c0c5caae20dd3db8bd0297 (diff) | |
download | UXP-e3b7744bee37c3d4a026d2193bed5e9439c40ff3.tar UXP-e3b7744bee37c3d4a026d2193bed5e9439c40ff3.tar.gz UXP-e3b7744bee37c3d4a026d2193bed5e9439c40ff3.tar.lz UXP-e3b7744bee37c3d4a026d2193bed5e9439c40ff3.tar.xz UXP-e3b7744bee37c3d4a026d2193bed5e9439c40ff3.zip |
Merge branch 'move-basilisk'
Diffstat (limited to 'application/basilisk/base/content/urlbarBindings.xml')
-rw-r--r-- | application/basilisk/base/content/urlbarBindings.xml | 2758 |
1 files changed, 2758 insertions, 0 deletions
diff --git a/application/basilisk/base/content/urlbarBindings.xml b/application/basilisk/base/content/urlbarBindings.xml new file mode 100644 index 000000000..eb3150581 --- /dev/null +++ b/application/basilisk/base/content/urlbarBindings.xml @@ -0,0 +1,2758 @@ +<?xml version="1.0"?> + +<!-- +-*- Mode: HTML -*- +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. +--> + +<!DOCTYPE bindings [ +<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd"> +%notificationDTD; +<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd"> +%browserDTD; +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%brandDTD; +]> + +<bindings id="urlbarBindings" xmlns="http://www.mozilla.org/xbl" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="urlbar" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete"> + + <content sizetopopup="pref"> + <xul:hbox anonid="textbox-container" + class="autocomplete-textbox-container urlbar-textbox-container" + flex="1" xbl:inherits="focused"> + <children includes="image|deck|stack|box"> + <xul:image class="autocomplete-icon" allowevents="true"/> + </children> + <xul:hbox anonid="textbox-input-box" + class="textbox-input-box urlbar-input-box" + flex="1" xbl:inherits="tooltiptext=inputtooltiptext"> + <children/> + <html:input anonid="input" + class="autocomplete-textbox urlbar-input textbox-input uri-element-right-align" + allowevents="true" + inputmode="url" + xbl:inherits="tooltiptext=inputtooltiptext,value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey"/> + </xul:hbox> + <xul:dropmarker anonid="historydropmarker" + class="autocomplete-history-dropmarker urlbar-history-dropmarker" + tooltiptext="&urlbar.openHistoryPopup.tooltip;" + allowevents="true" + xbl:inherits="open,enablehistory,parentfocused=focused"/> + <children includes="hbox"/> + </xul:hbox> + <xul:popupset anonid="popupset" + class="autocomplete-result-popupset"/> + <children includes="toolbarbutton"/> + </content> + + <implementation implements="nsIObserver, nsIDOMEventListener"> + <field name="AppConstants" readonly="true"> + (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants; + </field> +#ifdef MOZ_WEBEXTENSIONS + <field name="ExtensionSearchHandler" readonly="true"> + (Components.utils.import("resource://gre/modules/ExtensionSearchHandler.jsm", {})).ExtensionSearchHandler; + </field> +#endif + + <constructor><![CDATA[ + this._prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService) + .getBranch("browser.urlbar."); + + this._prefs.addObserver("", this, false); + this.clickSelectsAll = this._prefs.getBoolPref("clickSelectsAll"); + this.doubleClickSelectsAll = this._prefs.getBoolPref("doubleClickSelectsAll"); + this.completeDefaultIndex = this._prefs.getBoolPref("autoFill"); + this.timeout = this._prefs.getIntPref("delay"); + this._formattingEnabled = this._prefs.getBoolPref("formatting.enabled"); + this._mayTrimURLs = this._prefs.getBoolPref("trimURLs"); + this._cacheUserMadeSearchSuggestionsChoice(); + this.inputField.controllers.insertControllerAt(0, this._copyCutController); + this.inputField.addEventListener("paste", this, false); + this.inputField.addEventListener("mousedown", this, false); + this.inputField.addEventListener("mousemove", this, false); + this.inputField.addEventListener("mouseout", this, false); + this.inputField.addEventListener("overflow", this, false); + this.inputField.addEventListener("underflow", this, false); + + var textBox = document.getAnonymousElementByAttribute(this, + "anonid", "textbox-input-box"); + var cxmenu = document.getAnonymousElementByAttribute(textBox, + "anonid", "input-box-contextmenu"); + var pasteAndGo; + cxmenu.addEventListener("popupshowing", function() { + if (!pasteAndGo) + return; + var controller = document.commandDispatcher.getControllerForCommand("cmd_paste"); + var enabled = controller.isCommandEnabled("cmd_paste"); + if (enabled) + pasteAndGo.removeAttribute("disabled"); + else + pasteAndGo.setAttribute("disabled", "true"); + }, false); + + var insertLocation = cxmenu.firstChild; + while (insertLocation.nextSibling && + insertLocation.getAttribute("cmd") != "cmd_paste") + insertLocation = insertLocation.nextSibling; + if (insertLocation) { + pasteAndGo = document.createElement("menuitem"); + let label = Services.strings.createBundle("chrome://browser/locale/browser.properties"). + GetStringFromName("pasteAndGo.label"); + pasteAndGo.setAttribute("label", label); + pasteAndGo.setAttribute("anonid", "paste-and-go"); + pasteAndGo.setAttribute("oncommand", + "gURLBar.select(); goDoCommand('cmd_paste'); gURLBar.handleCommand();"); + cxmenu.insertBefore(pasteAndGo, insertLocation.nextSibling); + } + + this._enableOrDisableOneOffSearches(); + ]]></constructor> + + <destructor><![CDATA[ + this._prefs.removeObserver("", this); + this._prefs = null; + this.inputField.controllers.removeController(this._copyCutController); + this.inputField.removeEventListener("paste", this, false); + this.inputField.removeEventListener("mousedown", this, false); + this.inputField.removeEventListener("mousemove", this, false); + this.inputField.removeEventListener("mouseout", this, false); + this.inputField.removeEventListener("overflow", this, false); + this.inputField.removeEventListener("underflow", this, false); + ]]></destructor> + + <field name="_value">""</field> + <field name="gotResultForCurrentQuery">false</field> + + <!-- + This is set around HandleHenter so it can be used in handleCommand. + It is also used to track whether we must handle a delayed handleEnter, + by checking if it has been cleared. + --> + <field name="handleEnterInstance">null</field> + + <!-- + For performance reasons we want to limit the size of the text runs we + build and show to the user. + --> + <field name="textRunsMaxLen">255</field> + + <!-- + onBeforeValueGet is called by the base-binding's .value getter. + It can return an object with a "value" property, to override the + return value of the getter. + --> + <method name="onBeforeValueGet"> + <body><![CDATA[ + return { value: this._value }; + ]]></body> + </method> + + <!-- + onBeforeValueSet is called by the base-binding's .value setter. + It should return the value that the setter should use. + --> + <method name="onBeforeValueSet"> + <parameter name="aValue"/> + <body><![CDATA[ + this._value = aValue; + var returnValue = aValue; + var action = this._parseActionUrl(aValue); + + if (action) { + switch (action.type) { + case "switchtab": // Fall through. + case "remotetab": // Fall through. + case "visiturl": { + returnValue = action.params.displayUrl; + break; + } + case "keyword": // Fall through. + case "searchengine": { + returnValue = action.params.input; + break; + } + case "extension": { + returnValue = action.params.content; + break; + } + } + } else { + let originalUrl = ReaderMode.getOriginalUrl(aValue); + if (originalUrl) { + returnValue = originalUrl; + } + } + + // Set the actiontype only if the user is not overriding actions. + if (action && this._pressedNoActionKeys.size == 0) { + this.setAttribute("actiontype", action.type); + } else { + this.removeAttribute("actiontype"); + } + return returnValue; + ]]></body> + </method> + + <method name="onKeyPress"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_LEFT: + case KeyEvent.DOM_VK_RIGHT: + case KeyEvent.DOM_VK_HOME: + // Reset the selected index so that nsAutoCompleteController + // simply closes the popup without trying to fill anything. + this.popup.selectedIndex = -1; + break; + } + if (this.popup.popupOpen && + !this.popup.disableKeyNavigation && + this.popup.handleKeyPress(aEvent)) { + return true; + } + return this.handleKeyPress(aEvent); + ]]></body> + </method> + + <field name="_mayTrimURLs">true</field> + <method name="trimValue"> + <parameter name="aURL"/> + <body><![CDATA[ + // This method must not modify the given URL such that calling + // nsIURIFixup::createFixupURI with the result will produce a different URI. + return this._mayTrimURLs ? trimURL(aURL) : aURL; + ]]></body> + </method> + + <field name="_formattingEnabled">true</field> + <method name="formatValue"> + <body><![CDATA[ + if (!this._formattingEnabled || !this.editor) + return; + + let controller = this.editor.selectionController; + let strikeOut = controller.getSelection(controller.SELECTION_URLSTRIKEOUT); + strikeOut.removeAllRanges(); + + let selection = controller.getSelection(controller.SELECTION_URLSECONDARY); + selection.removeAllRanges(); + + if (this.focused) + return; + + let textNode = this.editor.rootElement.firstChild; + let value = textNode.textContent; + if (!value) + return; + + // Get the URL from the fixup service: + let flags = Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | + Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; + let uriInfo; + try { + uriInfo = Services.uriFixup.getFixupURIInfo(value, flags); + } catch (ex) {} + // Ignore if we couldn't make a URI out of this, the URI resulted in a search, + // or the URI has a non-http(s)/ftp protocol. + if (!uriInfo || + !uriInfo.fixedURI || + uriInfo.keywordProviderName || + ["http", "https", "ftp"].indexOf(uriInfo.fixedURI.scheme) == -1) { + return; + } + + // If we trimmed off the http scheme, ensure we stick it back on before + // trying to figure out what domain we're accessing, so we don't get + // confused by user:pass@host http URLs. We later use + // trimmedLength to ensure we don't count the length of a trimmed protocol + // when determining which parts of the URL to highlight as "preDomain". + let trimmedLength = 0; + if (uriInfo.fixedURI.scheme == "http" && !value.startsWith("http://")) { + value = "http://" + value; + trimmedLength = "http://".length; + } + + let matchedURL = value.match(/^((?:[a-z]+:\/\/)(?:[^\/#?]+@)?)(\S+?)(?::\d+)?\s*(?:[\/#?]|$)/); + if (!matchedURL) + return; + + // Strike out the "https" part if mixed active content is loaded. + if (this.getAttribute("pageproxystate") == "valid" && + value.startsWith("https:") && + gBrowser.securityUI.state & + Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) { + let range = document.createRange(); + range.setStart(textNode, 0); + range.setEnd(textNode, 5); + strikeOut.addRange(range); + } + + let [, preDomain, domain] = matchedURL; + let baseDomain = domain; + let subDomain = ""; + try { + baseDomain = Services.eTLD.getBaseDomainFromHost(uriInfo.fixedURI.host); + if (!domain.endsWith(baseDomain)) { + // getBaseDomainFromHost converts its resultant to ACE. + let IDNService = Cc["@mozilla.org/network/idn-service;1"] + .getService(Ci.nsIIDNService); + baseDomain = IDNService.convertACEtoUTF8(baseDomain); + } + } catch (e) {} + if (baseDomain != domain) { + subDomain = domain.slice(0, -baseDomain.length); + } + + let rangeLength = preDomain.length + subDomain.length - trimmedLength; + if (rangeLength) { + let range = document.createRange(); + range.setStart(textNode, 0); + range.setEnd(textNode, rangeLength); + selection.addRange(range); + } + + let startRest = preDomain.length + domain.length - trimmedLength; + if (startRest < value.length - trimmedLength) { + let range = document.createRange(); + range.setStart(textNode, startRest); + range.setEnd(textNode, value.length - trimmedLength); + selection.addRange(range); + } + ]]></body> + </method> + + <method name="handleRevert"> + <body><![CDATA[ + var isScrolling = this.popupOpen; + + gBrowser.userTypedValue = null; + + // don't revert to last valid url unless page is NOT loading + // and user is NOT key-scrolling through autocomplete list + if (!XULBrowserWindow.isBusy && !isScrolling) { + URLBarSetURI(); + + // If the value isn't empty and the urlbar has focus, select the value. + if (this.value && this.hasAttribute("focused")) + this.select(); + } + + // tell widget to revert to last typed text only if the user + // was scrolling when they hit escape + return !isScrolling; + ]]></body> + </method> + + <!-- + This is ultimately called by the autocomplete controller as the result + of handleEnter when the Return key is pressed in the textbox. Since + onPopupClick also calls handleEnter, this is also called as a result in + that case. + + @param event + The event that triggered the command. + @param openUILinkWhere + Optional. The "where" to pass to openUILinkIn. This method + computes the appropriate "where" given the event, but you can + use this to override it. + @param openUILinkParams + Optional. The parameters to pass to openUILinkIn. As with + "where", this method computes the appropriate parameters, but + any parameters you supply here will override those. + --> + <method name="handleCommand"> + <parameter name="event"/> + <parameter name="openUILinkWhere"/> + <parameter name="openUILinkParams"/> + <body><![CDATA[ + let isMouseEvent = event instanceof MouseEvent; + if (isMouseEvent && event.button == 2) { + // Do nothing for right clicks. + return; + } + + // Determine whether to use the selected one-off search button. In + // one-off search buttons parlance, "selected" means that the button + // has been navigated to via the keyboard. So we want to use it if + // the triggering event is not a mouse click -- i.e., it's a Return + // key -- or if the one-off was mouse-clicked. + let selectedOneOff = this.popup.oneOffSearchButtons.selectedButton; + if (selectedOneOff && + isMouseEvent && + event.originalTarget != selectedOneOff) { + selectedOneOff = null; + } + + // Do the command of the selected one-off if it's not an engine. + if (selectedOneOff && !selectedOneOff.engine) { + selectedOneOff.doCommand(); + return; + } + + let where = openUILinkWhere; + if (!where) { + if (isMouseEvent) { + where = whereToOpenLink(event, false, false); + } else { + // If the current tab is empty, ignore Alt+Enter (reuse this tab) + let altEnter = !isMouseEvent && + event && + event.altKey && + !isTabEmpty(gBrowser.selectedTab); + where = altEnter ? "tab" : "current"; + } + } + + let url = this.value; + if (!url) { + return; + } + + let mayInheritPrincipal = false; + let postData = null; + let browser = gBrowser.selectedBrowser; + let action = this._parseActionUrl(url); + + if (selectedOneOff && selectedOneOff.engine) { + // If there's a selected one-off button then load a search using + // the one-off's engine. + [url, postData] = + this._parseAndRecordSearchEngineLoad(selectedOneOff.engine, + this.oneOffSearchQuery, + event, where, + openUILinkParams); + } else if (action) { + switch (action.type) { + case "visiturl": + // Unifiedcomplete uses fixupURI to tell if something is a visit + // or a search, and passes out the fixedURI as the url param. + // By using that uri we would end up passing a different string + // to the docshell that may run a different not-found heuristic. + // For example, "mozilla/run" would be fixed by unifiedcomplete + // to "http://mozilla/run". The docshell, once it can't resolve + // mozilla, would note the string has a scheme, and try to load + // http://mozilla.com/run instead of searching "mozilla/run". + // So, if we have the original input at hand, we pass it through + // and let the docshell handle it. + if (action.params.input) { + url = action.params.input; + break; + } + url = action.params.url; + break; + case "remotetab": + url = action.params.url; + break; + case "keyword": + if (action.params.postData) { + postData = getPostDataStream(action.params.postData); + } + mayInheritPrincipal = true; + url = action.params.url; + break; + case "switchtab": + url = action.params.url; + if (this.hasAttribute("actiontype")) { + this.handleRevert(); + let prevTab = gBrowser.selectedTab; + if (switchToTabHavingURI(url) && isTabEmpty(prevTab)) { + gBrowser.removeTab(prevTab); + } + return; + } + break; + case "searchengine": + if (selectedOneOff && selectedOneOff.engine) { + // Replace the engine with the selected one-off engine. + action.params.engineName = selectedOneOff.engine.name; + } + const actionDetails = { + isSuggestion: !!action.params.searchSuggestion, + isAlias: !!action.params.alias + }; + [url, postData] = this._parseAndRecordSearchEngineLoad( + action.params.engineName, + action.params.searchSuggestion || action.params.searchQuery, + event, + where, + openUILinkParams, + actionDetails + ); + break; +#ifdef MOZ_WEBEXTENSIONS + case "extension": + this.handleRevert(); + // Give the extension control of handling the command. + let searchString = action.params.content; + let keyword = action.params.keyword; + this.ExtensionSearchHandler.handleInputEntered(keyword, searchString, where); + return; +#endif + } + } else { + // This is a fallback for add-ons and old testing code that directly + // set value and try to confirm it. UnifiedComplete should always + // resolve to a valid url. + try { + new URL(url); + } catch (ex) { + let lastLocationChange = browser.lastLocationChange; + getShortcutOrURIAndPostData(url).then(data => { + if (where != "current" || + browser.lastLocationChange == lastLocationChange) { + this._loadURL(data.url, browser, data.postData, where, + openUILinkParams, data.mayInheritPrincipal); + } + }); + return; + } + } + + this._loadURL(url, browser, postData, where, openUILinkParams, + mayInheritPrincipal); + ]]></body> + </method> + + <property name="oneOffSearchQuery"> + <getter><![CDATA[ + // this.textValue may be an autofilled string. Search only with the + // portion that the user typed, if any, by preferring the autocomplete + // controller's searchString (including handleEnterInstance.searchString). + return (this.handleEnterInstance && this.handleEnterInstance.searchString) || + this.mController.searchString || + this.textValue; + ]]></getter> + </property> + + <method name="_loadURL"> + <parameter name="url"/> + <parameter name="browser"/> + <parameter name="postData"/> + <parameter name="openUILinkWhere"/> + <parameter name="openUILinkParams"/> + <parameter name="mayInheritPrincipal"/> + <body><![CDATA[ + this.value = url; + browser.userTypedValue = url; + if (gInitialPages.includes(url)) { + browser.initialPageLoadedFromURLBar = url; + } + try { + addToUrlbarHistory(url); + } catch (ex) { + // Things may go wrong when adding url to session history, + // but don't let that interfere with the loading of the url. + Cu.reportError(ex); + } + + let params = { + postData, + allowThirdPartyFixup: true, + currentBrowser: browser, + }; + if (openUILinkWhere == "current") { + params.indicateErrorPageLoad = true; + params.allowPinnedTabHostChange = true; + params.disallowInheritPrincipal = !mayInheritPrincipal; + params.allowPopups = url.startsWith("javascript:"); + } else { + params.initiatingDoc = document; + } + + if (openUILinkParams) { + for (let key in openUILinkParams) { + params[key] = openUILinkParams[key]; + } + } + + // Focus the content area before triggering loads, since if the load + // occurs in a new tab, we want focus to be restored to the content + // area when the current tab is re-selected. + browser.focus(); + + if (openUILinkWhere != "current") { + this.handleRevert(); + } + + try { + openUILinkIn(url, openUILinkWhere, params); + } catch (ex) { + // This load can throw an exception in certain cases, which means + // we'll want to replace the URL with the loaded URL: + if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) { + this.handleRevert(); + } + } + + if (openUILinkWhere == "current") { + // Ensure the start of the URL is visible for usability reasons. + this.selectionStart = this.selectionEnd = 0; + } + ]]></body> + </method> + + <method name="_parseAndRecordSearchEngineLoad"> + <parameter name="engineOrEngineName"/> + <parameter name="query"/> + <parameter name="event"/> + <parameter name="openUILinkWhere"/> + <parameter name="openUILinkParams"/> + <parameter name="searchActionDetails"/> + <body><![CDATA[ + let engine = + typeof(engineOrEngineName) == "string" ? + Services.search.getEngineByName(engineOrEngineName) : + engineOrEngineName; + let isOneOff = this.popup.oneOffSearchButtons + .maybeRecordTelemetry(event, openUILinkWhere, openUILinkParams); + // Infer the type of the event which triggered the search. + let eventType = "unknown"; + if (event instanceof KeyboardEvent) { + eventType = "key"; + } else if (event instanceof MouseEvent) { + eventType = "mouse"; + } + // Augment the search action details object. + let details = searchActionDetails || {}; + details.isOneOff = isOneOff; + details.type = eventType; + + BrowserSearch.recordSearchInTelemetry(engine, "urlbar", details); + let submission = engine.getSubmission(query, null, "keyword"); + return [submission.uri.spec, submission.postData]; + ]]></body> + </method> + + <method name="maybeCanonizeURL"> + <parameter name="aTriggeringEvent"/> + <parameter name="aUrl"/> + <body><![CDATA[ + // Only add the suffix when the URL bar value isn't already "URL-like", + // and only if we get a keyboard event, to match user expectations. + if (!/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(aUrl) || + !(aTriggeringEvent instanceof KeyEvent)) { + return; + } + + let url = aUrl; + let accel = this.AppConstants.platform == "macosx" ? + aTriggeringEvent.metaKey : + aTriggeringEvent.ctrlKey; + let shift = aTriggeringEvent.shiftKey; + let suffix = ""; + + switch (true) { + case (accel && shift): + suffix = ".org/"; + break; + case (shift): + suffix = ".net/"; + break; + case (accel): + try { + suffix = gPrefService.getCharPref("browser.fixup.alternate.suffix"); + if (suffix.charAt(suffix.length - 1) != "/") + suffix += "/"; + } catch (e) { + suffix = ".com/"; + } + break; + } + + if (!suffix) + return; + + // trim leading/trailing spaces (bug 233205) + url = url.trim(); + + // Tack www. and suffix on. If user has appended directories, insert + // suffix before them (bug 279035). Be careful not to get two slashes. + let firstSlash = url.indexOf("/"); + if (firstSlash >= 0) { + url = url.substring(0, firstSlash) + suffix + + url.substring(firstSlash + 1); + } else { + url = url + suffix; + } + + this.popup.overrideValue = "http://www." + url; + ]]></body> + </method> + + <field name="_contentIsCropped">false</field> + + <method name="_initURLTooltip"> + <body><![CDATA[ + if (this.focused || !this._contentIsCropped) + return; + this.inputField.setAttribute("tooltiptext", this.value); + ]]></body> + </method> + + <method name="_hideURLTooltip"> + <body><![CDATA[ + this.inputField.removeAttribute("tooltiptext"); + ]]></body> + </method> + + <method name="_getDroppableLink"> + <parameter name="aEvent"/> + <body><![CDATA[ + let links = browserDragAndDrop.dropLinks(aEvent); + // The URL bar automatically handles inputs with newline characters, + // so we can get away with treating text/x-moz-url flavours as text/plain. + if (links.length > 0 && links[0].url) { + aEvent.preventDefault(); + let url = links[0].url; + let strippedURL = stripUnsafeProtocolOnPaste(url); + if (strippedURL != url) { + aEvent.stopImmediatePropagation(); + return null; + } + try { + urlSecurityCheck(url, + gBrowser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + } catch (ex) { + return null; + } + return url; + } + return null; + ]]></body> + </method> + + <method name="onDragOver"> + <parameter name="aEvent"/> + <body><![CDATA[ + // We don't need the link here, so we ignore the return value. + if (!this._getDroppableLink(aEvent)) { + aEvent.dataTransfer.dropEffect = "none"; + } + ]]></body> + </method> + + <method name="onDrop"> + <parameter name="aEvent"/> + <body><![CDATA[ + let url = this._getDroppableLink(aEvent); + if (url) { + this.value = url; + SetPageProxyState("invalid"); + this.focus(); + this.handleCommand(); + // Force not showing the dropped URI immediately. + gBrowser.userTypedValue = null; + URLBarSetURI(); + } + ]]></body> + </method> + + <method name="_getSelectedValueForClipboard"> + <body><![CDATA[ + // Grab the actual input field's value, not our value, which could include moz-action: + var inputVal = this.inputField.value; + var selectedVal = inputVal.substring(this.selectionStart, this.selectionEnd); + + // If the selection doesn't start at the beginning or doesn't span the full domain or + // the URL bar is modified or there is no text at all, nothing else to do here. + if (this.selectionStart > 0 || this.valueIsTyped || selectedVal == "") + return selectedVal; + // The selection doesn't span the full domain if it doesn't contain a slash and is + // followed by some character other than a slash. + if (!selectedVal.includes("/")) { + let remainder = inputVal.replace(selectedVal, ""); + if (remainder != "" && remainder[0] != "/") + return selectedVal; + } + + let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup); + + let uri; + if (this.getAttribute("pageproxystate") == "valid") { + uri = gBrowser.currentURI; + } else { + // We're dealing with an autocompleted value, create a new URI from that. + try { + uri = uriFixup.createFixupURI(inputVal, Ci.nsIURIFixup.FIXUP_FLAG_NONE); + } catch (e) {} + if (!uri) + return selectedVal; + } + + // Avoid copying 'about:reader?url=', and always provide the original URI: + let readerOriginalURL = ReaderMode.getOriginalUrl(uri.spec); + if (readerOriginalURL) { + uri = uriFixup.createFixupURI(readerOriginalURL, Ci.nsIURIFixup.FIXUP_FLAG_NONE); + } + + // Only copy exposable URIs + try { + uri = uriFixup.createExposableURI(uri); + } catch (ex) {} + + // If the entire URL is selected, just use the actual loaded URI. + if (inputVal == selectedVal) { + // ... but only if isn't a javascript: or data: URI, since those + // are hard to read when encoded + if (!uri.schemeIs("javascript") && !uri.schemeIs("data")) { + selectedVal = uri.spec; + } + + return selectedVal; + } + + // Just the beginning of the URL is selected, check for a trimmed + // value + let spec = uri.spec; + let trimmedSpec = this.trimValue(spec); + if (spec != trimmedSpec) { + // Prepend the portion that trimValue removed from the beginning. + // This assumes trimValue will only truncate the URL at + // the beginning or end (or both). + let trimmedSegments = spec.split(trimmedSpec); + selectedVal = trimmedSegments[0] + selectedVal; + } + + return selectedVal; + ]]></body> + </method> + + <field name="_copyCutController"><![CDATA[ + ({ + urlbar: this, + doCommand: function(aCommand) { + var urlbar = this.urlbar; + var val = urlbar._getSelectedValueForClipboard(); + if (!val) + return; + + if (aCommand == "cmd_cut" && this.isCommandEnabled(aCommand)) { + let start = urlbar.selectionStart; + let end = urlbar.selectionEnd; + urlbar.inputField.value = urlbar.inputField.value.substring(0, start) + + urlbar.inputField.value.substring(end); + urlbar.selectionStart = urlbar.selectionEnd = start; + + let event = document.createEvent("UIEvents"); + event.initUIEvent("input", true, false, window, 0); + urlbar.dispatchEvent(event); + + SetPageProxyState("invalid"); + } + + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(val); + }, + supportsCommand: function(aCommand) { + switch (aCommand) { + case "cmd_copy": + case "cmd_cut": + return true; + } + return false; + }, + isCommandEnabled: function(aCommand) { + return this.supportsCommand(aCommand) && + (aCommand != "cmd_cut" || !this.urlbar.readOnly) && + this.urlbar.selectionStart < this.urlbar.selectionEnd; + }, + onEvent: function(aEventName) {} + }) + ]]></field> + + <method name="observe"> + <parameter name="aSubject"/> + <parameter name="aTopic"/> + <parameter name="aData"/> + <body><![CDATA[ + if (aTopic == "nsPref:changed") { + switch (aData) { + case "clickSelectsAll": + case "doubleClickSelectsAll": + this[aData] = this._prefs.getBoolPref(aData); + break; + case "autoFill": + this.completeDefaultIndex = this._prefs.getBoolPref(aData); + break; + case "delay": + this.timeout = this._prefs.getIntPref(aData); + break; + case "formatting.enabled": + this._formattingEnabled = this._prefs.getBoolPref(aData); + break; + case "suggest.searches": + case "userMadeSearchSuggestionsChoice": + // Mirror the value for future use, see the comment in the + // binding's constructor. + this._prefs.setBoolPref("searchSuggestionsChoice", + this._prefs.getBoolPref("suggest.searches")); + + this._cacheUserMadeSearchSuggestionsChoice(); + if (this._userMadeSearchSuggestionsChoice) { + this.popup.searchSuggestionsNotificationWasDismissed( + this._prefs.getBoolPref("suggest.searches") + ); + } + break; + case "trimURLs": + this._mayTrimURLs = this._prefs.getBoolPref(aData); + break; + case "oneOffSearches": + this._enableOrDisableOneOffSearches(); + break; + } + } + ]]></body> + </method> + + <method name="_enableOrDisableOneOffSearches"> + <body><![CDATA[ + let enable = this._prefs.getBoolPref("oneOffSearches"); + this.popup.enableOneOffSearches(enable); + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "paste": + let originalPasteData = aEvent.clipboardData.getData("text/plain"); + if (!originalPasteData) { + return; + } + + let oldValue = this.inputField.value; + let oldStart = oldValue.substring(0, this.inputField.selectionStart); + // If there is already non-whitespace content in the URL bar + // preceding the pasted content, it's not necessary to check + // protocols used by the pasted content: + if (oldStart.trim()) { + return; + } + let oldEnd = oldValue.substring(this.inputField.selectionEnd); + + let pasteData = stripUnsafeProtocolOnPaste(originalPasteData); + if (originalPasteData != pasteData) { + // Unfortunately we're not allowed to set the bits being pasted + // so cancel this event: + aEvent.preventDefault(); + aEvent.stopImmediatePropagation(); + + this.inputField.value = oldStart + pasteData + oldEnd; + // Fix up cursor/selection: + let newCursorPos = oldStart.length + pasteData.length; + this.inputField.selectionStart = newCursorPos; + this.inputField.selectionEnd = newCursorPos; + } + break; + case "mousedown": + if (this.doubleClickSelectsAll && + aEvent.button == 0 && aEvent.detail == 2) { + this.editor.selectAll(); + aEvent.preventDefault(); + } + break; + case "mousemove": + this._initURLTooltip(); + break; + case "mouseout": + this._hideURLTooltip(); + break; + case "overflow": + this._contentIsCropped = true; + break; + case "underflow": + this._contentIsCropped = false; + this._hideURLTooltip(); + break; + } + ]]></body> + </method> + + <!-- + onBeforeTextValueSet is called by the base-binding's .textValue getter. + It should return the value that the getter should use. + --> + <method name="onBeforeTextValueGet"> + <body><![CDATA[ + return { value: this.inputField.value }; + ]]></body> + </method> + + <!-- + onBeforeTextValueSet is called by the base-binding's .textValue setter. + It should return the value that the setter should use. + --> + <method name="onBeforeTextValueSet"> + <parameter name="aValue"/> + <body><![CDATA[ + let val = aValue; + let uri; + try { + uri = makeURI(val); + } catch (ex) {} + + if (uri) { + // Do not touch moz-action URIs at all. They depend on being + // properly encoded and decoded and will break if decoded + // unexpectedly. + if (!this._parseActionUrl(val)) { + val = losslessDecodeURI(uri); + } + } + + return val; + ]]></body> + </method> + + <method name="_parseActionUrl"> + <parameter name="aUrl"/> + <body><![CDATA[ + const MOZ_ACTION_REGEX = /^moz-action:([^,]+),(.*)$/; + if (!MOZ_ACTION_REGEX.test(aUrl)) + return null; + + // URL is in the format moz-action:ACTION,PARAMS + // Where PARAMS is a JSON encoded object. + let [, type, params] = aUrl.match(MOZ_ACTION_REGEX); + + let action = { + type: type, + }; + + action.params = JSON.parse(params); + for (let key in action.params) { + action.params[key] = decodeURIComponent(action.params[key]); + } + + if ("url" in action.params) { + let uri; + try { + uri = makeURI(action.params.url); + action.params.displayUrl = losslessDecodeURI(uri); + } catch (e) { + action.params.displayUrl = action.params.url; + } + } + + return action; + ]]></body> + </method> + + <property name="_noActionKeys" readonly="true"> + <getter><![CDATA[ + if (!this.__noActionKeys) { + this.__noActionKeys = new Set([ + KeyEvent.DOM_VK_ALT, + KeyEvent.DOM_VK_SHIFT, + ]); + let modifier = this.AppConstants.platform == "macosx" ? + KeyEvent.DOM_VK_META : + KeyEvent.DOM_VK_CONTROL; + this.__noActionKeys.add(modifier); + } + return this.__noActionKeys; + ]]></getter> + </property> + + <field name="_pressedNoActionKeys"><![CDATA[ + new Set() + ]]></field> + + <method name="_clearNoActions"> + <parameter name="aURL"/> + <body><![CDATA[ + this._pressedNoActionKeys.clear(); + this.popup.removeAttribute("noactions"); + let action = this._parseActionUrl(this._value); + if (action) + this.setAttribute("actiontype", action.type); + ]]></body> + </method> + + <method name="onInput"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (!this.mIgnoreInput && this.mController.input == this) { + this._value = this.inputField.value; + gBrowser.userTypedValue = this.value; + this.valueIsTyped = true; + // Only wait for a result when we are sure to get one. In some + // cases, like when pasting the same exact text, we may not fire + // a new search and we won't get a result. + if (this.mController.handleText()) { + this.gotResultForCurrentQuery = false; + } + } + this.resetActionType(); + ]]></body> + </method> + + <method name="handleEnter"> + <parameter name="event"/> + <body><![CDATA[ + // We need to ensure we're using a selected autocomplete result. + // A result should automatically be selected by default, + // however autocomplete is async and therefore we may not + // have a result set relating to the current input yet. If that + // happens, we need to mark that when the first result does get added, + // it needs to be handled as if enter was pressed with that first + // result selected. + // If anything other than the default (first) result is selected, then + // it must have been manually selected by the human. We let this + // explicit choice be used, even if it may be related to a previous + // input. + // However, if the default result is automatically selected, we + // ensure that it corresponds to the current input. + + // Store the current search string so it can be used in + // handleCommand, which will be called as a result of + // mController.handleEnter(). + // Note this is also used to detect if we should perform a delayed + // handleEnter, in such a case it won't have been cleared. + this.handleEnterInstance = { + searchString: this.mController.searchString, + event: event + }; + + if (this.popup.selectedIndex != 0 || this.gotResultForCurrentQuery) { + this.maybeCanonizeURL(event, this.value); + let rv = this.mController.handleEnter(false, event); + this.handleEnterInstance = null; + this.popup.overrideValue = null; + return rv; + } + + return true; + ]]></body> + </method> + + <method name="handleDelete"> + <body><![CDATA[ + // If the heuristic result is selected, then the autocomplete + // controller's handleDelete implementation will remove it, which is + // not what we want. So in that case, call handleText so it acts as + // a backspace on the text value instead of removing the result. + if (this.popup.selectedIndex == 0 && + this.popup._isFirstResultHeuristic) { + this.mController.handleText(); + return false; + } + return this.mController.handleDelete(); + ]]></body> + </method> + + <field name="_userMadeSearchSuggestionsChoice"><![CDATA[ + false + ]]></field> + + <method name="_cacheUserMadeSearchSuggestionsChoice"> + <body><![CDATA[ + this._userMadeSearchSuggestionsChoice = + this._prefs.getBoolPref("userMadeSearchSuggestionsChoice") || + this._prefs.getBoolPref("suggest.searches"); + ]]></body> + </method> + + <property name="shouldShowSearchSuggestionsNotification" readonly="true"> + <getter><![CDATA[ + return !this._userMadeSearchSuggestionsChoice && + !this.inPrivateContext && + // When _urlbarFocused is true, tabbrowser would close the + // popup if it's opened here, so don't show the notification. + !gBrowser.selectedBrowser._urlbarFocused && + Services.prefs.getBoolPref("browser.search.suggest.enabled") && + this._prefs.getIntPref("daysBeforeHidingSuggestionsPrompt"); + ]]></getter> + </property> + + </implementation> + + <handlers> + <handler event="keydown"><![CDATA[ + if (this._noActionKeys.has(event.keyCode) && + this.popup.selectedIndex >= 0 && + !this._pressedNoActionKeys.has(event.keyCode)) { + if (this._pressedNoActionKeys.size == 0) { + this.popup.setAttribute("noactions", "true"); + this.removeAttribute("actiontype"); + } + this._pressedNoActionKeys.add(event.keyCode); + } + ]]></handler> + + <handler event="keyup"><![CDATA[ + if (this._noActionKeys.has(event.keyCode) && + this._pressedNoActionKeys.has(event.keyCode)) { + this._pressedNoActionKeys.delete(event.keyCode); + if (this._pressedNoActionKeys.size == 0) + this._clearNoActions(); + } + ]]></handler> + + <handler event="focus"><![CDATA[ + if (event.originalTarget == this.inputField) { + this._hideURLTooltip(); + this.formatValue(); + } + ]]></handler> + + <handler event="blur"><![CDATA[ + if (event.originalTarget == this.inputField) { + this._clearNoActions(); + this.formatValue(); + } +#ifdef MOZ_WEBEXTENSIONS + if (ExtensionSearchHandler.hasActiveInputSession()) { + ExtensionSearchHandler.handleInputCancelled(); + } +#endif + ]]></handler> + + <handler event="dragstart" phase="capturing"><![CDATA[ + // Drag only if the gesture starts from the input field. + if (this.inputField != event.originalTarget && + !(this.inputField.compareDocumentPosition(event.originalTarget) & + Node.DOCUMENT_POSITION_CONTAINED_BY)) + return; + + // Drag only if the entire value is selected and it's a valid URI. + var isFullSelection = this.selectionStart == 0 && + this.selectionEnd == this.textLength; + if (!isFullSelection || + this.getAttribute("pageproxystate") != "valid") + return; + + var urlString = gBrowser.selectedBrowser.currentURI.spec; + var title = gBrowser.selectedBrowser.contentTitle || urlString; + var htmlString = "<a href=\"" + urlString + "\">" + urlString + "</a>"; + + var dt = event.dataTransfer; + dt.setData("text/x-moz-url", urlString + "\n" + title); + dt.setData("text/unicode", urlString); + dt.setData("text/html", htmlString); + + dt.effectAllowed = "copyLink"; + event.stopPropagation(); + ]]></handler> + + <handler event="dragover" phase="capturing" action="this.onDragOver(event, this);"/> + <handler event="drop" phase="capturing" action="this.onDrop(event, this);"/> + <handler event="select"><![CDATA[ + if (!Cc["@mozilla.org/widget/clipboard;1"] + .getService(Ci.nsIClipboard) + .supportsSelectionClipboard()) + return; + + if (!window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .isHandlingUserInput) + return; + + var val = this._getSelectedValueForClipboard(); + if (!val) + return; + + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyStringToClipboard(val, Ci.nsIClipboard.kSelectionClipboard); + ]]></handler> + </handlers> + + </binding> + + <!-- Note: this binding is applied to the autocomplete popup used in web page content and extended in search.xml for the searchbar. --> + <binding id="browser-autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-result-popup"> + <implementation> + <field name="AppConstants" readonly="true"> + (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants; + </field> + + <method name="openAutocompletePopup"> + <parameter name="aInput"/> + <parameter name="aElement"/> + <body> + <![CDATA[ + // initially the panel is hidden + // to avoid impacting startup / new window performance + aInput.popup.hidden = false; + + // this method is defined on the base binding + this._openAutocompletePopup(aInput, aElement); + ]]></body> + </method> + + <method name="onPopupClick"> + <parameter name="aEvent"/> + <body><![CDATA[ + // Ignore all right-clicks + if (aEvent.button == 2) + return; + + var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController); + + var searchBar = BrowserSearch.searchBar; + var popupForSearchBar = searchBar && searchBar.textbox == this.mInput; + if (popupForSearchBar) { + searchBar.telemetrySearchDetails = { + index: controller.selection.currentIndex, + kind: "mouse" + }; + } + + // Check for unmodified left-click, and use default behavior + if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey && + !aEvent.altKey && !aEvent.metaKey) { + controller.handleEnter(true, aEvent); + return; + } + + // Check for middle-click or modified clicks on the search bar + if (popupForSearchBar) { + // Handle search bar popup clicks + var search = controller.getValueAt(this.selectedIndex); + + // open the search results according to the clicking subtlety + var where = whereToOpenLink(aEvent, false, true); + let params = {}; + + // But open ctrl/cmd clicks on autocomplete items in a new background tab. + let modifier = this.AppConstants.platform == "macosx" ? + aEvent.metaKey : + aEvent.ctrlKey; + if (where == "tab" && (aEvent instanceof MouseEvent) && + (aEvent.button == 1 || modifier)) + params.inBackground = true; + + // leave the popup open for background tab loads + if (!(where == "tab" && params.inBackground)) { + // close the autocomplete popup and revert the entered search term + this.closePopup(); + controller.handleEscape(); + } + + searchBar.doSearch(search, where, null, params); + if (where == "tab" && params.inBackground) + searchBar.focus(); + else + searchBar.value = search; + } + ]]></body> + </method> + </implementation> + </binding> + + <binding id="urlbar-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup"> + + <resources> + <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/> + <stylesheet src="chrome://browser/skin/searchbar.css"/> + </resources> + + <content ignorekeys="true" level="top" consumeoutsideclicks="never" + aria-owns="richlistbox"> + <xul:hbox anonid="search-suggestions-notification" + align="center" + role="alert" + aria-describedby="search-suggestions-notification-text"> + <xul:description flex="1"> + &urlbar.searchSuggestionsNotification.question; + <!-- Several things here are to make the label accessibile via an + accesskey so that a11y doesn't suck: the accesskey, using an + onclick handler instead of an href attribute, the control + attribute, and having the control attribute refer to a valid ID + that is the label itself. --> + <xul:label id="search-suggestions-notification-learn-more" + class="text-link" + role="link" + value="&urlbar.searchSuggestionsNotification.learnMore;" + accesskey="&urlbar.searchSuggestionsNotification.learnMore.accesskey;" + onclick="document.getBindingParent(this).openSearchSuggestionsNotificationLearnMoreURL();" + control="search-suggestions-notification-learn-more"/> + </xul:description> + <xul:button anonid="search-suggestions-notification-disable" + label="&urlbar.searchSuggestionsNotification.disable;" + accesskey="&urlbar.searchSuggestionsNotification.disable.accesskey;" + onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(false);"/> + <xul:button anonid="search-suggestions-notification-enable" + label="&urlbar.searchSuggestionsNotification.enable;" + accesskey="&urlbar.searchSuggestionsNotification.enable.accesskey;" + onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(true);"/> + </xul:hbox> + <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox" + flex="1"/> + <xul:hbox anonid="footer"> + <children/> + <xul:vbox anonid="one-off-search-buttons" + class="search-one-offs" + compact="true" + includecurrentengine="true" + disabletab="true" + flex="1"/> + </xul:hbox> + </content> + + <implementation> + <field name="_maxResults">0</field> + + <field name="_bundle" readonly="true"> + Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle("chrome://browser/locale/places/places.properties"); + </field> + + <field name="searchSuggestionsNotification" readonly="true"> + document.getAnonymousElementByAttribute( + this, "anonid", "search-suggestions-notification" + ); + </field> + + <field name="footer" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "footer"); + </field> + + <field name="oneOffSearchButtons" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", + "one-off-search-buttons"); + </field> + + <field name="_oneOffSearchesEnabled">false</field> + + <field name="_overrideValue">null</field> + <property name="overrideValue" + onget="return this._overrideValue;" + onset="this._overrideValue = val; return val;"/> + + <method name="onPopupClick"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (aEvent.button == 2) { + // Ignore right-clicks. + return; + } + // Otherwise "call super" -- do what autocomplete-base-popup does. + let controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController); + controller.handleEnter(true, aEvent); + ]]></body> + </method> + + <method name="enableOneOffSearches"> + <parameter name="enable"/> + <body><![CDATA[ + this._oneOffSearchesEnabled = enable; + if (enable) { + this.oneOffSearchButtons.telemetryOrigin = "urlbar"; + this.oneOffSearchButtons.style.display = "-moz-box"; + this.oneOffSearchButtons.popup = this; + this.oneOffSearchButtons.textbox = this.input; + } else { + this.oneOffSearchButtons.telemetryOrigin = null; + this.oneOffSearchButtons.style.display = "none"; + this.oneOffSearchButtons.popup = null; + this.oneOffSearchButtons.textbox = null; + } + ]]></body> + </method> + + <method name="openSearchSuggestionsNotificationLearnMoreURL"> + <body><![CDATA[ + let url = Services.urlFormatter.formatURL( + Services.prefs.getCharPref("app.support.baseURL") + "suggestions" + ); + openUILinkIn(url, "tab"); + ]]></body> + </method> + + <method name="dismissSearchSuggestionsNotification"> + <parameter name="enableSuggestions"/> + <body><![CDATA[ + // Make sure the urlbar is focused. It won't be, for example, if the + // user used an accesskey to make an opt-in choice. mIgnoreFocus + // prevents the text from being selected. + this.input.mIgnoreFocus = true; + this.input.focus(); + this.input.mIgnoreFocus = false; + + Services.prefs.setBoolPref( + "browser.urlbar.suggest.searches", enableSuggestions + ); + Services.prefs.setBoolPref( + "browser.urlbar.userMadeSearchSuggestionsChoice", true + ); + // The input's pref observer will now hide the notification. + ]]></body> + </method> + + <!-- Override this so that navigating between items results in an item + always being selected. --> + <method name="getNextIndex"> + <parameter name="reverse"/> + <parameter name="amount"/> + <parameter name="index"/> + <parameter name="maxRow"/> + <body><![CDATA[ + if (maxRow < 0) + return -1; + + let newIndex = index + (reverse ? -1 : 1) * amount; + + // We only want to wrap if navigation is in any direction by one item, + // otherwise we clamp to one end of the list. + // ie, hitting page-down will only cause is to wrap if we're already + // at one end of the list. + + // Allow the selection to be removed if the first result is not a + // heuristic result. + if (!this._isFirstResultHeuristic) { + if (reverse && index == -1 || newIndex > maxRow && index != maxRow) + newIndex = maxRow; + else if (!reverse && index == -1 || newIndex < 0 && index != 0) + newIndex = 0; + + if (newIndex < 0 && index == 0 || newIndex > maxRow && index == maxRow) + newIndex = -1; + + return newIndex; + } + + // Otherwise do not allow the selection to be removed. + if (newIndex < 0) { + newIndex = index > 0 ? 0 : maxRow; + } else if (newIndex > maxRow) { + newIndex = index < maxRow ? maxRow : 0; + } + return newIndex; + ]]></body> + </method> + + <property name="_isFirstResultHeuristic" readonly="true"> + <getter> + <![CDATA[ + // The popup usually has a special "heuristic" first result (added + // by UnifiedComplete.js) that is automatically selected when the + // popup opens. + return this.input.mController.matchCount > 0 && + this.input.mController + .getStyleAt(0) + .split(/\s+/).indexOf("heuristic") > 0; + ]]> + </getter> + </property> + + <property name="maxResults" readonly="true"> + <getter> + <![CDATA[ + if (!this._maxResults) { + var prefService = + Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + this._maxResults = prefService.getIntPref("browser.urlbar.maxRichResults"); + } + return this._maxResults; + ]]> + </getter> + </property> + + <method name="openAutocompletePopup"> + <parameter name="aInput"/> + <parameter name="aElement"/> + <body> + <![CDATA[ + // initially the panel is hidden + // to avoid impacting startup / new window performance + aInput.popup.hidden = false; + + let showNotification = aInput.shouldShowSearchSuggestionsNotification; + if (showNotification) { + let prefs = aInput._prefs; + let now = new Date(); + let date = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate(); + let previousDate = prefs.getIntPref("lastSuggestionsPromptDate"); + if (previousDate < date) { + let remainingDays = + prefs.getIntPref("daysBeforeHidingSuggestionsPrompt") - 1; + prefs.setIntPref("daysBeforeHidingSuggestionsPrompt", + remainingDays); + prefs.setIntPref("lastSuggestionsPromptDate", date); + if (!remainingDays) + showNotification = false; + } + } + + if (showNotification) { + this._showSearchSuggestionsNotification(); + } else if (this.classList.contains("showSearchSuggestionsNotification")) { + this._hideSearchSuggestionsNotification(); + } + + this._openAutocompletePopup(aInput, aElement); + ]]> + </body> + </method> + + <method name="_openAutocompletePopup"> + <parameter name="aInput"/> + <parameter name="aElement"/> + <body><![CDATA[ + if (this.mPopupOpen) { + return; + } + + this.mInput = aInput; + aInput.controller.setInitiallySelectedIndex(this._isFirstResultHeuristic ? 0 : -1); + this.view = aInput.controller.QueryInterface(Components.interfaces.nsITreeView); + this._invalidate(); + + var rect = window.document.documentElement.getBoundingClientRect(); + var width = rect.right - rect.left; + this.setAttribute("width", width); + + // Adjust the direction of the autocomplete popup list based on the textbox direction, bug 649840 + var popupDirection = aElement.ownerDocument.defaultView.getComputedStyle(aElement).direction; + this.style.direction = popupDirection; + + // Make the popup's starting margin negative so that the leading edge + // of the popup aligns with the window border. + let elementRect = aElement.getBoundingClientRect(); + if (popupDirection == "rtl") { + let offset = elementRect.right - rect.right + this.style.marginRight = offset + "px"; + } else { + let offset = rect.left - elementRect.left; + this.style.marginLeft = offset + "px"; + } + + // Keep the popup items' site icons aligned with the urlbar's identity + // icon if it's not too far from the edge of the window. If there are + // at most two toolbar buttons between the window edge and the urlbar, + // then consider that as "not too far." The forward button's + // visibility may have changed since the last time the popup was + // opened, so this needs to happen now. Do it *before* the popup + // opens because otherwise the items will visibly shift. + let nodes = [...document.getElementById("nav-bar-customization-target").childNodes]; + let urlbarPosition = nodes.findIndex(n => n.id == "urlbar-container"); + let alignSiteIcons = urlbarPosition <= 2 && + nodes.slice(0, urlbarPosition) + .every(n => n.localName == "toolbarbutton"); + if (alignSiteIcons) { + let identityRect = + document.getElementById("identity-icon").getBoundingClientRect(); + this.siteIconStart = popupDirection == "rtl" ? identityRect.right + : identityRect.left; + } + else { + // Reset the alignment so that the site icons are positioned + // according to whatever's in the CSS. + this.siteIconStart = undefined; + } + + // Position the popup below the navbar. To get the y-coordinate, + // which is an offset from the bottom of the input, subtract the + // bottom of the navbar from the buttom of the input. + let yOffset = + document.getElementById("nav-bar").getBoundingClientRect().bottom - + aInput.getBoundingClientRect().bottom; + this.openPopup(aElement, "after_start", 0, yOffset, false, false); + ]]></body> + </method> + + <method name="_updateFooterVisibility"> + <body> + <![CDATA[ + this.footer.collapsed = this._matchCount == 0; + ]]> + </body> + </method> + + <method name="_showSearchSuggestionsNotification"> + <body> + <![CDATA[ + // With the notification shown, the listbox's height can sometimes be + // too small when it's flexed, as it normally is. Also, it can start + // out slightly scrolled down. Both problems appear together, most + // often when the popup is very narrow and the notification's text + // must wrap. Work around them by removing the flex. + // + // But without flexing the listbox, the listbox's height animation + // sometimes fails to complete, leaving the popup too tall. Work + // around that problem by disabling the listbox animation. + this.richlistbox.flex = 0; + this.setAttribute("dontanimate", "true"); + + this.classList.add("showSearchSuggestionsNotification"); + this._updateFooterVisibility(); + + // This event allows accessibility APIs to see the notification. + if (!this.popupOpen) { + let event = document.createEvent("Events"); + event.initEvent("AlertActive", true, true); + this.searchSuggestionsNotification.dispatchEvent(event); + } + ]]> + </body> + </method> + + <method name="searchSuggestionsNotificationWasDismissed"> + <parameter name="enableSuggestions"/> + <body> + <![CDATA[ + if (!this.popupOpen) { + this._hideSearchSuggestionsNotification(); + return; + } + this._hideSearchSuggestionsNotificationWithAnimation().then(() => { + if (enableSuggestions && this.input.textValue) { + // Start a new search so that suggestions appear immediately. + this.input.controller.startSearch(this.input.textValue); + } + }); + ]]> + </body> + </method> + + <method name="_hideSearchSuggestionsNotification"> + <body> + <![CDATA[ + this.classList.remove("showSearchSuggestionsNotification"); + this.richlistbox.flex = 1; + this.removeAttribute("dontanimate"); + if (this._matchCount) { + // Update popup height. + this._invalidate(); + } else { + this.closePopup(); + } + ]]> + </body> + </method> + + <method name="_hideSearchSuggestionsNotificationWithAnimation"> + <body> + <![CDATA[ + return new Promise(resolve => { + let notificationHeight = this.searchSuggestionsNotification + .getBoundingClientRect() + .height; + this.searchSuggestionsNotification.style.marginTop = + "-" + notificationHeight + "px"; + + let popupHeightPx = + (this.getBoundingClientRect().height - notificationHeight) + "px"; + this.style.height = popupHeightPx; + + let onTransitionEnd = () => { + this.removeEventListener("transitionend", onTransitionEnd, true); + this.searchSuggestionsNotification.style.marginTop = "0px"; + this.style.removeProperty("height"); + this._hideSearchSuggestionsNotification(); + resolve(); + }; + this.addEventListener("transitionend", onTransitionEnd, true); + }); + ]]> + </body> + </method> + + <method name="_selectedOneOffChanged"> + <body><![CDATA[ + // Update all searchengine result items to use the newly selected + // engine. + for (let item of this.richlistbox.childNodes) { + if (item.collapsed) { + break; + } + let url = item.getAttribute("url"); + if (url) { + let action = item._parseActionUrl(url); + if (action && action.type == "searchengine") { + item._adjustAcItem(); + } + } + } + ]]></body> + </method> + + <!-- This handles keypress changes to the selection among the one-off + search buttons and between the one-offs and the listbox. It returns + true if the keypress was consumed and false if not. --> + <method name="handleKeyPress"> + <parameter name="aEvent"/> + <body><![CDATA[ + this.oneOffSearchButtons.handleKeyPress(aEvent, this._matchCount, + !this._isFirstResultHeuristic, + gBrowser.userTypedValue); + return aEvent.defaultPrevented; + ]]></body> + </method> + + <!-- This is called when a one-off is clicked and when "search in new tab" + is selected from a one-off context menu. --> + <method name="handleOneOffSearch"> + <parameter name="event"/> + <parameter name="engine"/> + <parameter name="where"/> + <parameter name="params"/> + <body><![CDATA[ + this.input.handleCommand(event, where, params); + ]]></body> + </method> + + <!-- Result listitems call this to determine which search engine they + should show in their labels and include in their url attributes. --> + <property name="overrideSearchEngineName" readonly="true"> + <getter><![CDATA[ + let button = this.oneOffSearchButtons.selectedButton; + return button && button.engine && button.engine.name; + ]]></getter> + </property> + + <method name="createResultLabel"> + <parameter name="item"/> + <parameter name="proposedLabel"/> + <body> + <![CDATA[ + let parts = [proposedLabel]; + + let action = this.mInput._parseActionUrl(item.getAttribute("url")); + if (action) { + switch (action.type) { + case "searchengine": + parts = [ + action.params.searchSuggestion || action.params.searchQuery, + action.params.engineName, + ]; + break; + case "switchtab": + case "remotetab": + parts = [ + item.getAttribute("title"), + item.getAttribute("displayurl"), + ]; + break; + } + } + + let types = item.getAttribute("type").split(/\s+/); + let type = types.find(type => type != "action" && type != "heuristic"); + try { + // Some types intentionally do not map to strings, which is not + // an error. + parts.push(this._bundle.GetStringFromName(type + "ResultLabel")); + } catch (e) {} + + return parts.filter(str => str).join(" "); + ]]> + </body> + </method> + + <method name="onResultsAdded"> + <body> + <![CDATA[ + // If nothing is selected yet, select the first result if it is a + // pre-selected "heuristic" result. (See UnifiedComplete.js.) + if (this.selectedIndex == -1 && this._isFirstResultHeuristic) { + // Don't fire DOMMenuItemActive so that screen readers still see + // the input as being focused. + this.richlistbox.suppressMenuItemEvent = true; + this.input.controller.setInitiallySelectedIndex(0); + this.richlistbox.suppressMenuItemEvent = false; + } + + this.input.gotResultForCurrentQuery = true; + + // Check if we should perform a delayed handleEnter. + if (this.input.handleEnterInstance) { + let instance = this.input.handleEnterInstance; + this.input.handleEnterInstance = null; + // Don't handle this immediately or we could cause a recursive + // loop where the controller sets popupOpen and re-enters here. + setTimeout(() => { + // Safety check: handle only if the search string didn't change. + let { event, searchString } = instance; + if (this.input.mController.searchString == searchString) { + this.input.maybeCanonizeURL(event, searchString); + this.input.mController.handleEnter(false, event); + this.overrideValue = null; + } + }, 0); + } + ]]> + </body> + </method> + + <method name="_onSearchBegin"> + <body><![CDATA[ + // Set the selected index to 0 (heuristic) until a result comes back + // and we can evaluate it better. + // + // This is required to properly manage delayed handleEnter: + // 1. if a search starts we set selectedIndex to 0 here, and it will + // be updated by onResultsAdded. Since selectedIndex is 0, + // handleEnter will delay the action if a result didn't arrive yet. + // 2. if a search doesn't start (for example if autocomplete is + // disabled), this won't be called, and the selectedIndex will be + // the default -1 value. Then handleEnter will know it should not + // delay the action, cause a result wont't ever arrive. + this.input.controller.setInitiallySelectedIndex(0); + ]]></body> + </method> + + </implementation> + <handlers> + + <handler event="SelectedOneOffButtonChanged"><![CDATA[ + this._selectedOneOffChanged(); + ]]></handler> + + <handler event="mousedown"><![CDATA[ + // Required to make the xul:label.text-link elements in the search + // suggestions notification work correctly when clicked on Linux. + // This is copied from the mousedown handler in + // browser-search-autocomplete-result-popup, which apparently had a + // similar problem. + event.preventDefault(); + ]]></handler> + + </handlers> + </binding> + + <binding id="addon-progress-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification"> + <implementation> + <constructor><![CDATA[ + if (!this.notification) + return; + + this.notification.options.installs.forEach(function(aInstall) { + aInstall.addListener(this); + }, this); + + // Calling updateProgress can sometimes cause this notification to be + // removed in the middle of refreshing the notification panel which + // makes the panel get refreshed again. Just initialise to the + // undetermined state and then schedule a proper check at the next + // opportunity + this.setProgress(0, -1); + this._updateProgressTimeout = setTimeout(this.updateProgress.bind(this), 0); + ]]></constructor> + + <destructor><![CDATA[ + this.destroy(); + ]]></destructor> + + <field name="progressmeter" readonly="true"> + document.getElementById("addon-progress-notification-progressmeter"); + </field> + <field name="progresstext" readonly="true"> + document.getElementById("addon-progress-notification-progresstext"); + </field> + <property name="DownloadUtils" readonly="true"> + <getter><![CDATA[ + let module = {}; + Components.utils.import("resource://gre/modules/DownloadUtils.jsm", module); + Object.defineProperty(this, "DownloadUtils", { + configurable: true, + enumerable: true, + writable: true, + value: module.DownloadUtils + }); + return module.DownloadUtils; + ]]></getter> + </property> + + <method name="destroy"> + <body><![CDATA[ + if (!this.notification) + return; + + this.notification.options.installs.forEach(function(aInstall) { + aInstall.removeListener(this); + }, this); + clearTimeout(this._updateProgressTimeout); + ]]></body> + </method> + + <method name="setProgress"> + <parameter name="aProgress"/> + <parameter name="aMaxProgress"/> + <body><![CDATA[ + if (aMaxProgress == -1) { + this.progressmeter.setAttribute("mode", "undetermined"); + } + else { + this.progressmeter.setAttribute("mode", "determined"); + this.progressmeter.setAttribute("value", (aProgress * 100) / aMaxProgress); + } + + let now = Date.now(); + + if (!this.notification.lastUpdate) { + this.notification.lastUpdate = now; + this.notification.lastProgress = aProgress; + return; + } + + let delta = now - this.notification.lastUpdate; + if ((delta < 400) && (aProgress < aMaxProgress)) + return; + + delta /= 1000; + + // This code is taken from nsDownloadManager.cpp + let speed = (aProgress - this.notification.lastProgress) / delta; + if (this.notification.speed) + speed = speed * 0.9 + this.notification.speed * 0.1; + + this.notification.lastUpdate = now; + this.notification.lastProgress = aProgress; + this.notification.speed = speed; + + let status = null; + [status, this.notification.last] = this.DownloadUtils.getDownloadStatus(aProgress, aMaxProgress, speed, this.notification.last); + this.progresstext.setAttribute("value", status); + this.progresstext.setAttribute("tooltiptext", status); + ]]></body> + </method> + + <method name="cancel"> + <body><![CDATA[ + let installs = this.notification.options.installs; + installs.forEach(function(aInstall) { + try { + aInstall.cancel(); + } + catch (e) { + // Cancel will throw if the download has already failed + } + }, this); + + PopupNotifications.remove(this.notification); + ]]></body> + </method> + + <method name="updateProgress"> + <body><![CDATA[ + if (!this.notification) + return; + + let downloadingCount = 0; + let progress = 0; + let maxProgress = 0; + + this.notification.options.installs.forEach(function(aInstall) { + if (aInstall.maxProgress == -1) + maxProgress = -1; + progress += aInstall.progress; + if (maxProgress >= 0) + maxProgress += aInstall.maxProgress; + if (aInstall.state < AddonManager.STATE_DOWNLOADED) + downloadingCount++; + }); + + if (downloadingCount == 0) { + this.destroy(); + if (Preferences.get("xpinstall.customConfirmationUI", false)) { + this.progressmeter.setAttribute("mode", "undetermined"); + let status = gNavigatorBundle.getString("addonDownloadVerifying"); + this.progresstext.setAttribute("value", status); + this.progresstext.setAttribute("tooltiptext", status); + } else { + PopupNotifications.remove(this.notification); + } + } + else { + this.setProgress(progress, maxProgress); + } + ]]></body> + </method> + + <method name="onDownloadProgress"> + <body><![CDATA[ + this.updateProgress(); + ]]></body> + </method> + + <method name="onDownloadFailed"> + <body><![CDATA[ + this.updateProgress(); + ]]></body> + </method> + + <method name="onDownloadCancelled"> + <body><![CDATA[ + this.updateProgress(); + ]]></body> + </method> + + <method name="onDownloadEnded"> + <body><![CDATA[ + this.updateProgress(); + ]]></body> + </method> + </implementation> + </binding> + + <binding id="plugin-popupnotification-center-item"> + <content align="center"> + <xul:vbox pack="center" anonid="itemBox" class="itemBox"> + <xul:description anonid="center-item-label" class="center-item-label" /> + <xul:hbox flex="1" pack="start" align="center" anonid="center-item-warning"> + <xul:image anonid="center-item-warning-icon" class="center-item-warning-icon"/> + <xul:label anonid="center-item-warning-label"/> + <xul:label anonid="center-item-link" value="&checkForUpdates;" class="text-link"/> + </xul:hbox> + </xul:vbox> + <xul:vbox pack="center"> + <xul:menulist class="center-item-menulist" + anonid="center-item-menulist"> + <xul:menupopup> + <xul:menuitem anonid="allownow" value="allownow" + label="&pluginActivateNow.label;" /> + <xul:menuitem anonid="allowalways" value="allowalways" + label="&pluginActivateAlways.label;" /> + <xul:menuitem anonid="block" value="block" + label="&pluginBlockNow.label;" /> + </xul:menupopup> + </xul:menulist> + </xul:vbox> + </content> + <resources> + <stylesheet src="chrome://global/skin/notification.css"/> + </resources> + <implementation> + <constructor><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "center-item-label").value = this.action.pluginName; + + let curState = "block"; + if (this.action.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) { + if (this.action.pluginPermissionType == Ci.nsIPermissionManager.EXPIRE_SESSION) { + curState = "allownow"; + } + else { + curState = "allowalways"; + } + } + document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").value = curState; + + let warningString = ""; + let linkString = ""; + + let link = document.getAnonymousElementByAttribute(this, "anonid", "center-item-link"); + + let url; + let linkHandler; + + if (this.action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED) { + document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").hidden = true; + warningString = gNavigatorBundle.getString("pluginActivateDisabled.label"); + linkString = gNavigatorBundle.getString("pluginActivateDisabled.manage"); + linkHandler = function(event) { + event.preventDefault(); + gPluginHandler.managePlugins(); + }; + document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning-icon").hidden = true; + } + else { + url = this.action.detailsLink; + + switch (this.action.blocklistState) { + case Ci.nsIBlocklistService.STATE_NOT_BLOCKED: + document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning").hidden = true; + break; + case Ci.nsIBlocklistService.STATE_BLOCKED: + document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").hidden = true; + warningString = gNavigatorBundle.getString("pluginActivateBlocked.label"); + linkString = gNavigatorBundle.getString("pluginActivate.learnMore"); + break; + case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE: + warningString = gNavigatorBundle.getString("pluginActivateOutdated.label"); + linkString = gNavigatorBundle.getString("pluginActivate.updateLabel"); + break; + case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE: + warningString = gNavigatorBundle.getString("pluginActivateVulnerable.label"); + linkString = gNavigatorBundle.getString("pluginActivate.riskLabel"); + break; + } + } + document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning-label").value = warningString; + + let chromeWin = window.QueryInterface(Ci.nsIDOMChromeWindow); + let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWin); + + if (isWindowPrivate) { + // TODO: temporary compromise of hiding some privacy leaks, remove once bug 892487 is fixed + let allowalways = document.getAnonymousElementByAttribute(this, "anonid", "allowalways"); + let block = document.getAnonymousElementByAttribute(this, "anonid", "block"); + let allownow = document.getAnonymousElementByAttribute(this, "anonid", "allownow"); + + allowalways.hidden = curState !== "allowalways"; + block.hidden = curState !== "block"; + allownow.hidden = curState === "allowalways"; + } + + if (url || linkHandler) { + link.value = linkString; + if (url) { + link.href = url; + } + if (linkHandler) { + link.addEventListener("click", linkHandler, false); + } + } + else { + link.hidden = true; + } + ]]></constructor> + <property name="value"> + <getter> + return document.getAnonymousElementByAttribute(this, "anonid", + "center-item-menulist").value; + </getter> + <setter><!-- This should be used only in automated tests --> + document.getAnonymousElementByAttribute(this, "anonid", + "center-item-menulist").value = val; + </setter> + </property> + </implementation> + </binding> + + <binding id="click-to-play-plugins-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification"> + <content align="start" style="width: &pluginNotification.width;;"> + <xul:vbox flex="1" align="stretch" class="popup-notification-main-box" + xbl:inherits="popupid"> + <xul:hbox class="click-to-play-plugins-notification-description-box" flex="1" align="start"> + <xul:description class="click-to-play-plugins-outer-description" flex="1"> + <html:span anonid="click-to-play-plugins-notification-description" /> + <xul:label class="text-link click-to-play-plugins-notification-link" anonid="click-to-play-plugins-notification-link" /> + </xul:description> + <xul:toolbarbutton anonid="closebutton" + class="messageCloseButton popup-notification-closebutton tabbable close-icon" + xbl:inherits="oncommand=closebuttoncommand" + tooltiptext="&closeNotification.tooltip;"/> + </xul:hbox> + <xul:grid anonid="click-to-play-plugins-notification-center-box" + class="click-to-play-plugins-notification-center-box"> + <xul:columns> + <xul:column flex="1"/> + <xul:column/> + </xul:columns> + <xul:rows> + <children includes="row"/> + <xul:hbox pack="start" anonid="plugin-notification-showbox"> + <xul:button label="&pluginNotification.showAll.label;" + accesskey="&pluginNotification.showAll.accesskey;" + class="plugin-notification-showbutton" + oncommand="document.getBindingParent(this)._setState(2)"/> + </xul:hbox> + </xul:rows> + </xul:grid> + <xul:hbox anonid="button-container" + class="click-to-play-plugins-notification-button-container" + pack="center" align="center"> + <xul:button anonid="primarybutton" + class="click-to-play-popup-button" + oncommand="document.getBindingParent(this)._onButton(this)" + flex="1"/> + <xul:button anonid="secondarybutton" + class="click-to-play-popup-button" + oncommand="document.getBindingParent(this)._onButton(this);" + flex="1"/> + </xul:hbox> + <xul:box hidden="true"> + <children/> + </xul:box> + </xul:vbox> + </content> + <resources> + <stylesheet src="chrome://global/skin/notification.css"/> + </resources> + <implementation> + <field name="_states"> + ({SINGLE: 0, MULTI_COLLAPSED: 1, MULTI_EXPANDED: 2}) + </field> + <field name="_primaryButton"> + document.getAnonymousElementByAttribute(this, "anonid", "primarybutton"); + </field> + <field name="_secondaryButton"> + document.getAnonymousElementByAttribute(this, "anonid", "secondarybutton") + </field> + <field name="_buttonContainer"> + document.getAnonymousElementByAttribute(this, "anonid", "button-container") + </field> + <field name="_brandShortName"> + document.getElementById("bundle_brand").getString("brandShortName") + </field> + <field name="_items">[]</field> + <constructor><![CDATA[ + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let sortedActions = []; + for (let action of this.notification.options.pluginData.values()) { + sortedActions.push(action); + } + sortedActions.sort((a, b) => a.pluginName.localeCompare(b.pluginName)); + + for (let action of sortedActions) { + let item = document.createElementNS(XUL_NS, "row"); + item.setAttribute("class", "plugin-popupnotification-centeritem"); + item.action = action; + this.appendChild(item); + this._items.push(item); + } + switch (this._items.length) { + case 0: + PopupNotifications._dismiss(); + break; + case 1: + this._setState(this._states.SINGLE); + break; + default: + if (this.notification.options.primaryPlugin) { + this._setState(this._states.MULTI_COLLAPSED); + } else { + this._setState(this._states.MULTI_EXPANDED); + } + } + ]]></constructor> + <method name="_setState"> + <parameter name="state" /> + <body><![CDATA[ + var grid = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-center-box"); + + if (this._states.SINGLE == state) { + grid.hidden = true; + this._setupSingleState(); + return; + } + + let prePath = this.notification.options.principal.URI.prePath; + this._setupDescription("pluginActivateMultiple.message", null, prePath); + + var showBox = document.getAnonymousElementByAttribute(this, "anonid", "plugin-notification-showbox"); + + var dialogStrings = Services.strings.createBundle("chrome://global/locale/dialog.properties"); + this._primaryButton.label = dialogStrings.GetStringFromName("button-accept"); + this._primaryButton.setAttribute("default", "true"); + + this._secondaryButton.label = dialogStrings.GetStringFromName("button-cancel"); + this._primaryButton.setAttribute("action", "_multiAccept"); + this._secondaryButton.setAttribute("action", "_cancel"); + + grid.hidden = false; + + if (this._states.MULTI_COLLAPSED == state) { + for (let child of this.childNodes) { + if (child.tagName != "row") { + continue; + } + child.hidden = this.notification.options.primaryPlugin != + child.action.permissionString; + } + showBox.hidden = false; + } + else { + for (let child of this.childNodes) { + if (child.tagName != "row") { + continue; + } + child.hidden = false; + } + showBox.hidden = true; + } + this._setupLink(null); + ]]></body> + </method> + <method name="_setupSingleState"> + <body><![CDATA[ + var action = this._items[0].action; + var prePath = action.pluginPermissionPrePath; + let chromeWin = window.QueryInterface(Ci.nsIDOMChromeWindow); + let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWin); + + let label, linkLabel, button1, button2; + + if (action.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) { + button1 = { + label: "pluginBlockNow.label", + accesskey: "pluginBlockNow.accesskey", + action: "_singleBlock" + }; + button2 = { + label: "pluginContinue.label", + accesskey: "pluginContinue.accesskey", + action: "_singleContinue", + default: true + }; + switch (action.blocklistState) { + case Ci.nsIBlocklistService.STATE_NOT_BLOCKED: + label = "pluginEnabled.message"; + linkLabel = "pluginActivate.learnMore"; + break; + + case Ci.nsIBlocklistService.STATE_BLOCKED: + Cu.reportError(Error("Cannot happen!")); + break; + + case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE: + label = "pluginEnabledOutdated.message"; + linkLabel = "pluginActivate.updateLabel"; + break; + + case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE: + label = "pluginEnabledVulnerable.message"; + linkLabel = "pluginActivate.riskLabel" + break; + + default: + Cu.reportError(Error("Unexpected blocklist state")); + } + + // TODO: temporary compromise, remove this once bug 892487 is fixed + if (isWindowPrivate) { + this._buttonContainer.hidden = true; + } + } + else if (action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED) { + let linkElement = + document.getAnonymousElementByAttribute( + this, "anonid", "click-to-play-plugins-notification-link"); + linkElement.textContent = gNavigatorBundle.getString("pluginActivateDisabled.manage"); + linkElement.setAttribute("onclick", "gPluginHandler.managePlugins()"); + + let descElement = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description"); + descElement.textContent = gNavigatorBundle.getFormattedString( + "pluginActivateDisabled.message", [action.pluginName, this._brandShortName]) + " "; + this._buttonContainer.hidden = true; + return; + } + else if (action.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) { + let descElement = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description"); + descElement.textContent = gNavigatorBundle.getFormattedString( + "pluginActivateBlocked.message", [action.pluginName, this._brandShortName]) + " "; + this._setupLink("pluginActivate.learnMore", action.detailsLink); + this._buttonContainer.hidden = true; + return; + } + else { + button1 = { + label: "pluginActivateNow.label", + accesskey: "pluginActivateNow.accesskey", + action: "_singleActivateNow" + }; + button2 = { + label: "pluginActivateAlways.label", + accesskey: "pluginActivateAlways.accesskey", + action: "_singleActivateAlways" + }; + switch (action.blocklistState) { + case Ci.nsIBlocklistService.STATE_NOT_BLOCKED: + label = "pluginActivateNew.message"; + linkLabel = "pluginActivate.learnMore"; + button2.default = true; + break; + + case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE: + label = "pluginActivateOutdated.message"; + linkLabel = "pluginActivate.updateLabel"; + button1.default = true; + break; + + case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE: + label = "pluginActivateVulnerable.message"; + linkLabel = "pluginActivate.riskLabel" + button1.default = true; + break; + + default: + Cu.reportError(Error("Unexpected blocklist state")); + } + + // TODO: temporary compromise, remove this once bug 892487 is fixed + if (isWindowPrivate) { + button1.default = true; + this._secondaryButton.hidden = true; + } + } + this._setupDescription(label, action.pluginName, prePath); + this._setupLink(linkLabel, action.detailsLink); + + this._primaryButton.label = gNavigatorBundle.getString(button1.label); + this._primaryButton.accessKey = gNavigatorBundle.getString(button1.accesskey); + this._primaryButton.setAttribute("action", button1.action); + + this._secondaryButton.label = gNavigatorBundle.getString(button2.label); + this._secondaryButton.accessKey = gNavigatorBundle.getString(button2.accesskey); + this._secondaryButton.setAttribute("action", button2.action); + if (button1.default) { + this._primaryButton.setAttribute("default", "true"); + } + else if (button2.default) { + this._secondaryButton.setAttribute("default", "true"); + } + ]]></body> + </method> + <method name="_setupDescription"> + <parameter name="baseString" /> + <parameter name="pluginName" /> <!-- null for the multiple-plugin case --> + <parameter name="prePath" /> + <body><![CDATA[ + var span = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description"); + while (span.lastChild) { + span.removeChild(span.lastChild); + } + + var args = ["__prepath__", this._brandShortName]; + if (pluginName) { + args.unshift(pluginName); + } + var bases = gNavigatorBundle.getFormattedString(baseString, args). + split("__prepath__", 2); + + span.appendChild(document.createTextNode(bases[0])); + var prePathSpan = document.createElementNS("http://www.w3.org/1999/xhtml", "em"); + prePathSpan.appendChild(document.createTextNode(prePath)); + span.appendChild(prePathSpan); + span.appendChild(document.createTextNode(bases[1] + " ")); + ]]></body> + </method> + <method name="_setupLink"> + <parameter name="linkString"/> + <parameter name="linkUrl" /> + <body><![CDATA[ + var link = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-link"); + if (!linkString || !linkUrl) { + link.hidden = true; + return; + } + + link.hidden = false; + link.textContent = gNavigatorBundle.getString(linkString); + link.href = linkUrl; + ]]></body> + </method> + <method name="_onButton"> + <parameter name="aButton" /> + <body><![CDATA[ + let methodName = aButton.getAttribute("action"); + this[methodName](); + ]]></body> + </method> + <method name="_singleActivateNow"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this._items[0].action, + "allownow"); + this._cancel(); + ]]></body> + </method> + <method name="_singleBlock"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this._items[0].action, + "block"); + this._cancel(); + ]]></body> + </method> + <method name="_singleActivateAlways"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this._items[0].action, + "allowalways"); + this._cancel(); + ]]></body> + </method> + <method name="_singleContinue"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this._items[0].action, + "continue"); + this._cancel(); + ]]></body> + </method> + <method name="_multiAccept"> + <body><![CDATA[ + for (let item of this._items) { + let action = item.action; + if (action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED || + action.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) { + continue; + } + gPluginHandler._updatePluginPermission(this.notification, + item.action, item.value); + } + this._cancel(); + ]]></body> + </method> + <method name="_cancel"> + <body><![CDATA[ + PopupNotifications._dismiss(); + ]]></body> + </method> + <method name="_accept"> + <parameter name="aEvent" /> + <body><![CDATA[ + if (aEvent.defaultPrevented) + return; + aEvent.preventDefault(); + if (this._primaryButton.getAttribute("default") == "true") { + this._primaryButton.click(); + } + else if (this._secondaryButton.getAttribute("default") == "true") { + this._secondaryButton.click(); + } + ]]></body> + </method> + </implementation> + <handlers> + <!-- The _accept method checks for .defaultPrevented so that if focus is in a button, + enter activates the button and not this default action --> + <handler event="keypress" keycode="VK_RETURN" group="system" action="this._accept(event);"/> + </handlers> + </binding> + + <binding id="splitmenu"> + <content> + <xul:hbox anonid="menuitem" flex="1" + class="splitmenu-menuitem" + xbl:inherits="iconic,label,disabled,onclick=oncommand,_moz-menuactive=active"/> + <xul:menu anonid="menu" class="splitmenu-menu" + xbl:inherits="disabled,_moz-menuactive=active" + oncommand="event.stopPropagation();"> + <children includes="menupopup"/> + </xul:menu> + </content> + + <implementation implements="nsIDOMEventListener"> + <constructor><![CDATA[ + this._parentMenupopup.addEventListener("DOMMenuItemActive", this, false); + this._parentMenupopup.addEventListener("popuphidden", this, false); + ]]></constructor> + + <destructor><![CDATA[ + this._parentMenupopup.removeEventListener("DOMMenuItemActive", this, false); + this._parentMenupopup.removeEventListener("popuphidden", this, false); + ]]></destructor> + + <field name="menuitem" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "menuitem"); + </field> + <field name="menu" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "menu"); + </field> + + <field name="_menuDelay">600</field> + + <field name="_parentMenupopup"><![CDATA[ + this._getParentMenupopup(this); + ]]></field> + + <method name="_getParentMenupopup"> + <parameter name="aNode"/> + <body><![CDATA[ + let node = aNode.parentNode; + while (node) { + if (node.localName == "menupopup") + break; + node = node.parentNode; + } + return node; + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="event"/> + <body><![CDATA[ + switch (event.type) { + case "DOMMenuItemActive": + if (this.getAttribute("active") == "true" && + event.target != this && + this._getParentMenupopup(event.target) == this._parentMenupopup) + this.removeAttribute("active"); + break; + case "popuphidden": + if (event.target == this._parentMenupopup) + this.removeAttribute("active"); + break; + } + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="mouseover"><![CDATA[ + if (this.getAttribute("active") != "true") { + this.setAttribute("active", "true"); + + let event = document.createEvent("Events"); + event.initEvent("DOMMenuItemActive", true, false); + this.dispatchEvent(event); + + if (this.getAttribute("disabled") != "true") { + let self = this; + setTimeout(function () { + if (self.getAttribute("active") == "true") + self.menu.open = true; + }, this._menuDelay); + } + } + ]]></handler> + + <handler event="popupshowing"><![CDATA[ + if (event.target == this.firstChild && + this._parentMenupopup._currentPopup) + this._parentMenupopup._currentPopup.hidePopup(); + ]]></handler> + + <handler event="click" phase="capturing"><![CDATA[ + if (this.getAttribute("disabled") == "true") { + // Prevent the command from being carried out + event.stopPropagation(); + return; + } + + let node = event.originalTarget; + while (true) { + if (node == this.menuitem) + break; + if (node == this) + return; + node = node.parentNode; + } + + this._parentMenupopup.hidePopup(); + ]]></handler> + </handlers> + </binding> + + <binding id="menuitem-tooltip" extends="chrome://global/content/bindings/menu.xml#menuitem"> + <implementation> + <constructor><![CDATA[ + this.setAttribute("tooltiptext", this.getAttribute("acceltext")); + // TODO: Simplify this to this.setAttribute("acceltext", "") once bug + // 592424 is fixed + document.getAnonymousElementByAttribute(this, "anonid", "accel").firstChild.setAttribute("value", ""); + ]]></constructor> + </implementation> + </binding> + + <binding id="menuitem-iconic-tooltip" extends="chrome://global/content/bindings/menu.xml#menuitem-iconic"> + <implementation> + <constructor><![CDATA[ + this.setAttribute("tooltiptext", this.getAttribute("acceltext")); + // TODO: Simplify this to this.setAttribute("acceltext", "") once bug + // 592424 is fixed + document.getAnonymousElementByAttribute(this, "anonid", "accel").firstChild.setAttribute("value", ""); + ]]></constructor> + </implementation> + </binding> +</bindings> |