From e72ef92b5bdc43cd2584198e2e54e951b70299e8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 03:32:58 -0500 Subject: Add Basilisk --- .../basilisk/components/search/content/search.xml | 2325 ++++++++++++++++++++ .../components/search/content/searchReset.js | 90 + .../components/search/content/searchReset.xhtml | 61 + .../search/content/searchbarBindings.css | 18 + application/basilisk/components/search/jar.mn | 9 + application/basilisk/components/search/moz.build | 7 + 6 files changed, 2510 insertions(+) create mode 100644 application/basilisk/components/search/content/search.xml create mode 100644 application/basilisk/components/search/content/searchReset.js create mode 100644 application/basilisk/components/search/content/searchReset.xhtml create mode 100644 application/basilisk/components/search/content/searchbarBindings.css create mode 100644 application/basilisk/components/search/jar.mn create mode 100644 application/basilisk/components/search/moz.build (limited to 'application/basilisk/components/search') diff --git a/application/basilisk/components/search/content/search.xml b/application/basilisk/components/search/content/search.xml new file mode 100644 index 000000000..3b675df57 --- /dev/null +++ b/application/basilisk/components/search/content/search.xml @@ -0,0 +1,2325 @@ + + + + +%searchBarDTD; + +%browserDTD; +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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(); + ]]> + + + + + + + + + + + + + + + + + + + + + + + + { + if (navigator.platform.startsWith("Mac") && aEvent.keyCode == KeyEvent.VK_F4) + this.openSearch() + }, true); + + this.controllers.appendController(this.searchbarController); + document.getBindingParent(this)._textboxInitialized = true; + + // Add observer for suggest preference + Services.prefs.addObserver("browser.search.suggest.enabled", this, false); + ]]> + + + + + + + + + + + + + + + + + + + 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 + tree.collapsed = !tree.view || !tree.view.rowCount; + } + + // Show the current default engine in the top header of the panel. + this.updateHeader(); + ]]> + + { + this._isHiding = false; + }, 0); + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + null + + + + + + + + null + + + + + + + + + "" + + "" + + + + + + + + null + + + + + + + + + + + + + + + + { + return button.getAttribute("selected") == "true"; + }); + ]]> + + + + + + + + + + + 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(); + }); + ]]> + + + + + { + this.selectedButton = null; + this._contextEngine = null; + }, Ci.nsIThread.DISPATCH_NORMAL); + break; + } + ]]> + + + + + + + + + + + + + + with:" header. + this._updateAfterQueryChanged(); + + let list = document.getAnonymousElementByAttribute(this, "anonid", + "search-panel-one-offs"); + + // 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. + this._rebuildAddEngineList(); + + let settingsButton = + document.getAnonymousElementByAttribute(this, "anonid", + "search-settings-compact"); + // Finally, build the list of one-off buttons. + while (list.firstChild != settingsButton) + list.firstChild.remove(); + // Remove the trailing empty text node introduced by the binding's + // content markup above. + if (settingsButton.nextSibling) + settingsButton.nextSibling.remove(); + + let Preferences = + Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences; + let pref = Preferences.get("browser.search.hiddenOneOffs"); + let hiddenList = pref ? pref.split(",") : []; + + let currentEngineName = Services.search.currentEngine.name; + let includeCurrentEngine = this.getAttribute("includecurrentengine"); + let engines = Services.search.getVisibleEngines().filter(e => { + return (includeCurrentEngine || e.name != currentEngineName) && + !hiddenList.includes(e.name); + }); + + let header = document.getAnonymousElementByAttribute(this, "anonid", + "search-panel-one-offs-header") + // header is a xul:deck so collapsed doesn't work on it, see bug 589569. + header.hidden = list.collapsed = !engines.length; + + if (!engines.length) + return; + + let panelWidth = parseInt(this.popup.clientWidth); + // 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. + let oneOffCount = engines.length; + if (this.compact) + ++oneOffCount; + let rowCount = Math.ceil(oneOffCount / enginesPerRow); + let height = rowCount * 33; // 32px per row, 1px border. + list.setAttribute("height", height + "px"); + + // Ensure we can refer to the settings buttons by ID: + let settingsEl = document.getAnonymousElementByAttribute(this, "anonid", "search-settings"); + settingsEl.id = this.telemetryOrigin + "-anon-search-settings"; + let compactSettingsEl = document.getAnonymousElementByAttribute(this, "anonid", "search-settings-compact"); + compactSettingsEl.id = this.telemetryOrigin + + "-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"); + + list.insertBefore(button, settingsButton); + } + + 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"); + + list.insertBefore(button, settingsButton); + } + + if (this.compact) { + this.settingsButton.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. + // + // There's one weird thing to guard against. When layout pixels + // aren't an integral multiple of device pixels, the calculated + // remainder can end up being ~1px too big, at least on Windows, + // which pushes the settings button to a new row. The remainder + // is integral, not a fraction, so that's not the problem. To + // work around that, unscale the remainder, floor it, scale it + // back, and then floor that. + let scale = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .screenPixelsPerCSSPixel; + let remainder = panelWidth - (enginesPerRow * buttonWidth); + remainder = Math.floor(Math.floor(remainder * scale) / scale); + let width = remainder + buttonWidth; + let lastDummyItem = this.settingsButton.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.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 && index < buttons.length) + this.selectedButton = buttons[index]; + else + this.selectedButton = null; + + if (this.selectedButton || aWrapAround) + return true; + + return false; + } + + // If no selection, select the first button or ... + if (aForward) { + this.selectedButton = buttons[0]; + return true; + } + + if (!aForward && aWrapAround) { + // the last button. + this.selectedButton = buttons[buttons.length - 1]; + return true; + } + + return false; + ]]> + + + + + + + + + 0) { + if (this.popup.selectedIndex > 0) { + // The autocomplete controller should handle this case. + } else if (this.popup.selectedIndex == 0) { + if (!allowEmptySelection) { + // Wrap around the selection to the last one-off. + this.selectedButton = null; + this.popup.selectedIndex = -1; + // Call advanceSelection after setting selectedIndex so that + // screen readers see the newly selected one-off. Both trigger + // accessibility events. + this.advanceSelection(false, true, true); + stopEvent = true; + } + } else { + let firstButtonSelected = + this.selectedButton && + this.selectedButton == this.getSelectableButtons(true)[0]; + if (firstButtonSelected) { + this.selectedButton = null; + } else { + stopEvent = this.advanceSelection(false, true, true); + } + } + } else { + stopEvent = this.advanceSelection(false, true, true); + } + } else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) { + if (numListItems > 0) { + if (this.popup.selectedIndex >= 0 && + this.popup.selectedIndex < numListItems - 1) { + // The autocomplete controller should handle this case. + } else if (this.popup.selectedIndex == numListItems - 1) { + this.selectedButton = null; + if (!allowEmptySelection) { + this.popup.selectedIndex = -1; + stopEvent = true; + } + if (this.textbox && typeof(textboxUserValue) == "string") { + this.textbox.value = textboxUserValue; + } + // Call advanceSelection after setting selectedIndex so that + // screen readers see the newly selected one-off. Both trigger + // accessibility events. + this.advanceSelection(true, true, true); + } else { + let buttons = this.getSelectableButtons(true); + let lastButtonSelected = + this.selectedButton && + this.selectedButton == buttons[buttons.length - 1]; + if (lastButtonSelected) { + this.selectedButton = null; + stopEvent = allowEmptySelection; + } else if (this.selectedButton) { + stopEvent = this.advanceSelection(true, true, true); + } else { + // The autocomplete controller should handle this case. + } + } + } else { + stopEvent = this.advanceSelection(true, true, true); + } + } else if (this.selectedButton && + this.selectedButton.getAttribute("anonid") == + "addengine-menu-button" && + event.keyCode == KeyEvent.DOM_VK_RIGHT) { + // If the add-engine overflow menu item is selected and the user + // presses the right arrow key, open the submenu. Unfortunately + // handling the left arrow key -- to close the popup -- isn't + // straightforward. Once the popup is open, it consumes all key + // events. Setting ignorekeys=handled on it doesn't help, since the + // popup handles all arrow keys. Setting ignorekeys=true on it does + // mean that the popup no longer consumes the left arrow key, but then + // it no longer handles up/down keys to select items in the popup. + this.selectedButton.open = true; + stopEvent = true; + } + + if (stopEvent) { + event.preventDefault(); + event.stopPropagation(); + return true; + } + 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; + } + ]]> + + + + + + + diff --git a/application/basilisk/components/search/content/searchReset.js b/application/basilisk/components/search/content/searchReset.js new file mode 100644 index 000000000..b541d41da --- /dev/null +++ b/application/basilisk/components/search/content/searchReset.js @@ -0,0 +1,90 @@ +/* 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/. */ + +"use strict"; + +var {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); + +const TELEMETRY_RESULT_ENUM = { + RESTORED_DEFAULT: 0, + KEPT_CURRENT: 1, + CHANGED_ENGINE: 2, + CLOSED_PAGE: 3, + OPENED_SETTINGS: 4 +}; + +window.onload = function() { + let defaultEngine = document.getElementById("defaultEngine"); + let originalDefault = Services.search.originalDefaultEngine; + defaultEngine.textContent = originalDefault.name; + defaultEngine.style.backgroundImage = + 'url("' + originalDefault.iconURI.spec + '")'; + + document.getElementById("searchResetChangeEngine").focus(); + window.addEventListener("unload", recordPageClosed); + document.getElementById("linkSettingsPage") + .addEventListener("click", openingSettings); +}; + +function doSearch() { + let queryString = ""; + let purpose = ""; + let params = window.location.href.match(/^about:searchreset\?([^#]*)/); + if (params) { + params = params[1].split("&"); + for (let param of params) { + if (param.startsWith("data=")) + queryString = decodeURIComponent(param.slice(5)); + else if (param.startsWith("purpose=")) + purpose = param.slice(8); + } + } + + let engine = Services.search.currentEngine; + let submission = engine.getSubmission(queryString, null, purpose); + + window.removeEventListener("unload", recordPageClosed); + + let win = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + win.openUILinkIn(submission.uri.spec, "current", false, submission.postData); +} + +function openingSettings() { + record(TELEMETRY_RESULT_ENUM.OPENED_SETTINGS); + window.removeEventListener("unload", recordPageClosed); +} + +function record(result) { + Services.telemetry.getHistogramById("SEARCH_RESET_RESULT").add(result); +} + +function keepCurrentEngine() { + // Calling the currentEngine setter will force a correct loadPathHash to be + // written for this engine, so that we don't prompt the user again. + Services.search.currentEngine = Services.search.currentEngine; + record(TELEMETRY_RESULT_ENUM.KEPT_CURRENT); + doSearch(); +} + +function changeSearchEngine() { + let engine = Services.search.originalDefaultEngine; + if (engine.hidden) + engine.hidden = false; + Services.search.currentEngine = engine; + + record(TELEMETRY_RESULT_ENUM.RESTORED_DEFAULT); + + doSearch(); +} + +function recordPageClosed() { + record(TELEMETRY_RESULT_ENUM.CLOSED_PAGE); +} diff --git a/application/basilisk/components/search/content/searchReset.xhtml b/application/basilisk/components/search/content/searchReset.xhtml new file mode 100644 index 000000000..b851dd383 --- /dev/null +++ b/application/basilisk/components/search/content/searchReset.xhtml @@ -0,0 +1,61 @@ + + + + + + %htmlDTD; + + %globalDTD; + + %searchresetDTD; + + %brandDTD; +]> + + + + &searchreset.tabtitle; + + + + +