%browserDTD; ]> { window.requestIdleCallback(() => { Services.search.init(aStatus => { // Bail out if the binding's been destroyed if (!this._initialized) return; if (Components.isSuccessCode(aStatus)) { // Refresh the display (updating icon, etc) this.updateDisplay(); BrowserSearch.updateOpenSearchBadge(); } else { Components.utils.reportError("Cannot initialize search service, bailing out: " + aStatus); } }); }); }); // Wait until the popupshowing event to avoid forcing immediate // attachment of the search-one-offs binding. this.textbox.popup.addEventListener("popupshowing", () => { let oneOffButtons = this.textbox.popup.oneOffButtons; // Some accessibility tests create their own that doesn't // use the popup binding below, so null-check oneOffButtons. if (oneOffButtons) { oneOffButtons.telemetryOrigin = "searchbar"; // Set .textbox first, since the popup setter will cause // a _rebuild call that uses it. oneOffButtons.textbox = this.textbox; oneOffButtons.popup = this.textbox.popup; } }, {capturing: true, once: true}); ]]> false false document.getAnonymousElementByAttribute(this, "anonid", "searchbar-stringbundle"); false document.getAnonymousElementByAttribute(this, "anonid", "searchbar-textbox"); null (Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory; = 0 && newIndex < this.engines.length) { this.currentEngine = this.engines[newIndex]; } aEvent.preventDefault(); aEvent.stopPropagation(); this.openSuggestionsPanel(); ]]> this.currentEngine = engine }; Services.search.addEngine(target.getAttribute("uri"), null, target.getAttribute("src"), false, installCallback); } else return; this.focus(); this.select(); ]]> { this.initContextMenu(cxmenu); }, {capturing: true, once: true}); this.setAttribute("aria-owns", this.popup.id); document.getBindingParent(this)._textboxInitialized = true; ]]> // Add items to context menu and attach controller to handle them the // first time the context menu is opened. { if (aEvent.keyCode == KeyEvent.DOM_VK_F4) this.openSearch(); }, true); } this.controllers.appendController(this.searchbarController); let onpopupshowing = function() { BrowserSearch.searchBar._textbox.closePopup(); if (suggestMenuItem) { let enabled = Services.prefs.getBoolPref("browser.search.suggest.enabled"); suggestMenuItem.setAttribute("checked", enabled); } if (!pasteAndSearch) return; let controller = document.commandDispatcher.getControllerForCommand("cmd_paste"); let enabled = controller.isCommandEnabled("cmd_paste"); if (enabled) pasteAndSearch.removeAttribute("disabled"); else pasteAndSearch.setAttribute("disabled", "true"); }; aMenu.addEventListener("popupshowing", onpopupshowing); onpopupshowing(); ]]> 100 ? width : 100); var yOffset = outerRect.bottom - innerRect.bottom; popup.openPopup(this.inputField, "after_start", 0, yOffset, false, false); } ]]> false null document.getAnonymousElementByAttribute(this, "anonid", "search-one-off-buttons"); = 1 row. // The autocomplete binding itself will take care of uncollapsing later, // if we currently have no rows but end up having some in the future // when the search string changes this.tree.collapsed = !this.tree.view || !this.tree.view.rowCount; } // Show the current default engine in the top header of the panel. this.updateHeader(); ]]> { this._isHiding = false; }); ]]> null null 0 "" "" null document.getAnonymousElementByAttribute(this, "anonid", "search-panel-one-offs"); document.getAnonymousElementByAttribute(this, "anonid", "search-panel-one-offs-header"); document.getAnonymousElementByAttribute(this, "anonid", "add-engines"); document.getAnonymousElementByAttribute(this, "anonid", "search-settings"); document.getAnonymousElementByAttribute(this, "anonid", "search-settings-compact"); null null aEvent.stopPropagation(); menu.addEventListener("popupshowing", listener); menu.addEventListener("popuphiding", listener); menu.addEventListener("popupshown", aEvent => { this._ignoreMouseEvents = true; aEvent.stopPropagation(); }); menu.addEventListener("popuphidden", aEvent => { this._ignoreMouseEvents = false; aEvent.stopPropagation(); }); // Add weak referenced observers to invalidate our cached list of engines. Services.prefs.addObserver("browser.search.hiddenOneOffs", this, true); Services.obs.addObserver(this, "browser-search-engine-modified", true); // Rebuild the buttons when the theme changes. See bug 1357800 for // details. Summary: On Linux, switching between themes can cause a row // of buttons to disappear. Services.obs.addObserver(this, "lightweight-theme-changed", true); ]]> { this.selectedButton = null; this._contextEngine = null; }); break; } ]]> null { let name = e.name; return (!currentEngineNameToIgnore || name != currentEngineNameToIgnore) && !hiddenList.includes(name); }); return this._engines; ]]> with:" header. this._updateAfterQueryChanged(); // Handle opensearch items. This needs to be done before building the // list of one off providers, as that code will return early if all the // alternative engines are hidden. // Skip this in compact mode, ie. for the urlbar. if (!this.compact) this._rebuildAddEngineList(); // Check if the one-off buttons really need to be rebuilt. if (this._textbox) { // We can't get a reliable value for the popup width without flushing, // but the popup width won't change if the textbox width doesn't. let DOMUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); let textboxWidth = DOMUtils.getBoundsWithoutFlushing(this._textbox).width; // We can return early if neither the list of engines nor the panel // width has changed. if (this._engines && this._textboxWidth == textboxWidth) { return; } this._textboxWidth = textboxWidth; } // Finally, build the list of one-off buttons. while (this.buttons.firstChild != this.settingsButtonCompact) this.buttons.firstChild.remove(); // Remove the trailing empty text node introduced by the binding's // content markup above. if (this.settingsButtonCompact.nextSibling) this.settingsButtonCompact.nextSibling.remove(); let engines = this.engines; let oneOffCount = engines.length; // header is a xul:deck so collapsed doesn't work on it, see bug 589569. this.header.hidden = this.buttons.collapsed = !oneOffCount; if (!oneOffCount) return; let panelWidth = parseInt(this.popup.clientWidth); // There's one weird thing to guard against: when layout pixels // aren't an integral multiple of device pixels, the last button // of each row sometimes gets pushed to the next row, depending on the // panel and button widths. // This is likely because the clientWidth getter rounds the value, but // the panel's border width is not an integer. // As a workaround, decrement the width if the scale is not an integer. let scale = window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils) .screenPixelsPerCSSPixel; if (Math.floor(scale) != scale) { --panelWidth; } // The + 1 is because the last button doesn't have a right border. let enginesPerRow = Math.floor((panelWidth + 1) / this.buttonWidth); let buttonWidth = Math.floor(panelWidth / enginesPerRow); // There will be an emtpy area of: // panelWidth - enginesPerRow * buttonWidth px // at the end of each row. // If the tag with the list of search engines doesn't have // a fixed height, the panel will be sized incorrectly, causing the bottom // of the suggestion to be hidden. if (this.compact) ++oneOffCount; let rowCount = Math.ceil(oneOffCount / enginesPerRow); let height = rowCount * 33; // 32px per row, 1px border. this.buttons.setAttribute("height", height + "px"); // Ensure we can refer to the settings buttons by ID: let origin = this.telemetryOrigin; this.settingsButton.id = origin + "-anon-search-settings"; this.settingsButtonCompact.id = origin + "-anon-search-settings-compact"; const kXULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; let dummyItems = enginesPerRow - (oneOffCount % enginesPerRow || enginesPerRow); for (let i = 0; i < engines.length; ++i) { let engine = engines[i]; let button = document.createElementNS(kXULNS, "button"); button.id = this._buttonIDForEngine(engine); let uri = "chrome://browser/skin/search-engine-placeholder.png"; if (engine.iconURI) { uri = engine.iconURI.spec; } button.setAttribute("image", uri); button.setAttribute("class", "searchbar-engine-one-off-item"); button.setAttribute("tooltiptext", engine.name); button.setAttribute("width", buttonWidth); button.engine = engine; if ((i + 1) % enginesPerRow == 0) button.classList.add("last-of-row"); if (i + 1 == engines.length) button.classList.add("last-engine"); if (i >= oneOffCount + dummyItems - enginesPerRow) button.classList.add("last-row"); this.buttons.insertBefore(button, this.settingsButtonCompact); } let hasDummyItems = !!dummyItems; while (dummyItems) { let button = document.createElementNS(kXULNS, "button"); button.setAttribute("class", "searchbar-engine-one-off-item dummy last-row"); button.setAttribute("width", buttonWidth); if (!--dummyItems) button.classList.add("last-of-row"); this.buttons.insertBefore(button, this.settingsButtonCompact); } if (this.compact) { this.settingsButtonCompact.setAttribute("width", buttonWidth); if (rowCount == 1 && hasDummyItems) { // When there's only one row, make the compact settings button // hug the right edge of the panel. It may not due to the panel's // width not being an integral multiple of the button width. (See // the "There will be an emtpy area" comment above.) Increase the // width of the last dummy item by the remainder. let remainder = panelWidth - (enginesPerRow * buttonWidth); let width = remainder + buttonWidth; let lastDummyItem = this.settingsButtonCompact.previousSibling; lastDummyItem.setAttribute("width", width); } } ]]> 5 this._addEngineMenuThreshold; if (tooManyEngines) { // Make the top-level menu button. let button = document.createElementNS(kXULNS, "button"); list.appendChild(button); button.classList.add("addengine-item"); button.setAttribute("anonid", "addengine-menu-button"); button.setAttribute("type", "menu"); button.setAttribute("label", this.bundle.GetStringFromName("cmd_addFoundEngineMenu")); button.setAttribute("crop", "end"); button.setAttribute("pack", "start"); // Set the menu button's image to the image of the first engine. The // offered engines may have differing images, so there's no perfect // choice here. let engine = engines[0]; if (engine.icon) { button.setAttribute("image", engine.icon); } // Now make the button's child menupopup. list = document.createElementNS(kXULNS, "menupopup"); button.appendChild(list); list.setAttribute("anonid", "addengine-menu"); list.setAttribute("position", "topright topleft"); // Events from child menupopups bubble up to the autocomplete binding, // which breaks it, so prevent these events from propagating. let suppressEventTypes = [ "popupshowing", "popuphiding", "popupshown", "popuphidden", ]; for (let type of suppressEventTypes) { list.addEventListener(type, event => { event.stopPropagation(); }); } } // Finally, add the engines to the list. If there aren't too many // engines, the list is the add-engines vbox. Otherwise it's the // menupopup created earlier. In the latter case, create menuitem // elements instead of buttons, because buttons don't get keyboard // handling for free inside menupopups. let eltType = tooManyEngines ? "menuitem" : "button"; for (let engine of engines) { let button = document.createElementNS(kXULNS, eltType); button.classList.add("addengine-item"); button.id = this.telemetryOrigin + "-add-engine-" + this._fixUpEngineNameForID(engine.title); let label = this.bundle.formatStringFromName("cmd_addFoundEngine", [engine.title], 1); button.setAttribute("label", label); button.setAttribute("crop", "end"); button.setAttribute("tooltiptext", engine.title + "\n" + engine.uri); button.setAttribute("uri", engine.uri); button.setAttribute("title", engine.title); if (engine.icon) { button.setAttribute("image", engine.icon); } if (tooManyEngines) { button.classList.add("menuitem-iconic"); } else { button.setAttribute("pack", "start"); } list.appendChild(button); } ]]> 0) { // Moving up within the list. The autocomplete controller should // handle this case. A button may be selected, so null it. this.selectedButton = null; return false; } if (this.popup.selectedIndex == 0) { // Moving up from the top of the list. if (allowEmptySelection) { // Let the autocomplete controller remove selection in the list // and revert the typed text in the textbox. return false; } // Wrap selection around to the last button. if (this.textbox && typeof(textboxUserValue) == "string") { this.textbox.value = textboxUserValue; } this.advanceSelection(false, true, true); return true; } if (!this.selectedButton) { // Moving up from no selection in the list or the buttons, back // down to the last button. this.advanceSelection(false, true, true); return true; } if (this.selectedButtonIndex == 0) { // Moving up from the buttons to the bottom of the list. this.selectedButton = null; return false; } // Moving up/left within the buttons. this.advanceSelection(false, true, false); return true; } if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) { if (event.altKey) { // Keep the currently selected result in the list (if any) as a // secondary "alt" selection and move the selection down within // the buttons. this.advanceSelection(true, false, false); return true; } if (numListItems == 0) { this.advanceSelection(true, true, false); return true; } if (this.popup.selectedIndex >= 0 && this.popup.selectedIndex < numListItems - 1) { // Moving down within the list. The autocomplete controller // should handle this case. A button may be selected, so null it. this.selectedButton = null; return false; } if (this.popup.selectedIndex == numListItems - 1) { // Moving down from the last item in the list to the buttons. this.selectedButtonIndex = 0; if (allowEmptySelection) { // Let the autocomplete controller remove selection in the list // and revert the typed text in the textbox. return false; } if (this.textbox && typeof(textboxUserValue) == "string") { this.textbox.value = textboxUserValue; } this.popup.selectedIndex = -1; return true; } if (this.selectedButton) { let buttons = this.getSelectableButtons(true); if (this.selectedButtonIndex == buttons.length - 1) { // Moving down from the buttons back up to the top of the list. this.selectedButton = null; if (allowEmptySelection) { // Prevent the selection from wrapping around to the top of // the list by returning true, since the list currently has no // selection. Nothing should be selected after handling this // Down key. return true; } return false; } // Moving down/right within the buttons. this.advanceSelection(true, true, false); return true; } return false; } if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_LEFT) { if (this.selectedButton && (this.compact || this.selectedButton.engine)) { // Moving left within the buttons. this.advanceSelection(false, this.compact, true); return true; } return false; } if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RIGHT) { if (this.selectedButton && (this.compact || this.selectedButton.engine)) { // Moving right within the buttons. this.advanceSelection(true, this.compact, true); return true; } return false; } return false; ]]> 200 null false { delete this._addEngineMenuTimeout; let button = document.getAnonymousElementByAttribute( this, "anonid", "addengine-menu-button" ); button.open = this._addEngineMenuShouldBeOpen; }, this._addEngineMenuTimeoutMs); ]]> { this._rebuild(); }, onError(errorCode) { if (errorCode != Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE) { // Download error is shown by the search service return; } const kSearchBundleURI = "chrome://global/locale/search/search.properties"; let searchBundle = Services.strings.createBundle(kSearchBundleURI); let brandBundle = document.getElementById("bundle_brand"); let brandName = brandBundle.getString("brandShortName"); let title = searchBundle.GetStringFromName("error_invalid_engine_title"); let text = searchBundle.formatStringFromName("error_duplicate_engine_msg", [brandName, target.getAttribute("uri")], 2); Services.prompt.QueryInterface(Ci.nsIPromptFactory); let prompt = Services.prompt.getPrompt(gBrowser.contentWindow, Ci.nsIPrompt); prompt.QueryInterface(Ci.nsIWritablePropertyBag2); prompt.setPropertyAsBool("allowTabModal", true); prompt.alert(title, text); } }; Services.search.addEngine(target.getAttribute("uri"), null, target.getAttribute("image"), false, installCallback); } let anonid = target.getAttribute("anonid"); if (anonid == "search-one-offs-context-open-in-new-tab") { // Select the context-clicked button so that consumers can easily // tell which button was acted on. this.selectedButton = this._buttonForEngine(this._contextEngine); this.handleSearchCommand(event, this._contextEngine, true); } if (anonid == "search-one-offs-context-set-default") { let currentEngine = Services.search.currentEngine; if (!this.getAttribute("includecurrentengine")) { // Make the target button of the context menu reflect the current // search engine first. Doing this as opposed to rebuilding all the // one-off buttons avoids flicker. let button = this._buttonForEngine(this._contextEngine); button.id = this._buttonIDForEngine(currentEngine); let uri = "chrome://browser/skin/search-engine-placeholder.png"; if (currentEngine.iconURI) uri = currentEngine.iconURI.spec; button.setAttribute("image", uri); button.setAttribute("tooltiptext", currentEngine.name); button.engine = currentEngine; } Services.search.currentEngine = this._contextEngine; } ]]>