forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			751 lines
		
	
	
	
		
			25 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			751 lines
		
	
	
	
		
			25 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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";
 | |
| 
 | |
| this.ContentSearchUIController = (function () {
 | |
| 
 | |
| const MAX_DISPLAYED_SUGGESTIONS = 6;
 | |
| const SUGGESTION_ID_PREFIX = "searchSuggestion";
 | |
| const ONE_OFF_ID_PREFIX = "oneOff";
 | |
| const CSS_URI = "chrome://browser/content/contentSearchUI.css";
 | |
| 
 | |
| const HTML_NS = "http://www.w3.org/1999/xhtml";
 | |
| 
 | |
| /**
 | |
|  * Creates a new object that manages search suggestions and their UI for a text
 | |
|  * box.
 | |
|  *
 | |
|  * The UI consists of an html:table that's inserted into the DOM after the given
 | |
|  * text box and styled so that it appears as a dropdown below the text box.
 | |
|  *
 | |
|  * @param inputElement
 | |
|  *        Search suggestions will be based on the text in this text box.
 | |
|  *        Assumed to be an html:input.  xul:textbox is untested but might work.
 | |
|  * @param tableParent
 | |
|  *        The suggestion table is appended as a child to this element.  Since
 | |
|  *        the table is absolutely positioned and its top and left values are set
 | |
|  *        to be relative to the top and left of the page, either the parent and
 | |
|  *        all its ancestors should not be positioned elements (i.e., their
 | |
|  *        positions should be "static"), or the parent's position should be the
 | |
|  *        top left of the page.
 | |
|  * @param healthReportKey
 | |
|  *        This will be sent with the search data for FHR to record the search.
 | |
|  * @param searchPurpose
 | |
|  *        Sent with search data, see nsISearchEngine.getSubmission.
 | |
|  * @param idPrefix
 | |
|  *        The IDs of elements created by the object will be prefixed with this
 | |
|  *        string.
 | |
|  */
 | |
| function ContentSearchUIController(inputElement, tableParent, healthReportKey,
 | |
|                                    searchPurpose, idPrefix="") {
 | |
|   this.input = inputElement;
 | |
|   this._idPrefix = idPrefix;
 | |
|   this._healthReportKey = healthReportKey;
 | |
|   this._searchPurpose = searchPurpose;
 | |
| 
 | |
|   let tableID = idPrefix + "searchSuggestionTable";
 | |
|   this.input.autocomplete = "off";
 | |
|   this.input.setAttribute("aria-autocomplete", "true");
 | |
|   this.input.setAttribute("aria-controls", tableID);
 | |
|   tableParent.appendChild(this._makeTable(tableID));
 | |
| 
 | |
|   this.input.addEventListener("keypress", this);
 | |
|   this.input.addEventListener("input", this);
 | |
|   this.input.addEventListener("focus", this);
 | |
|   this.input.addEventListener("blur", this);
 | |
|   window.addEventListener("ContentSearchService", this);
 | |
| 
 | |
|   this._stickyInputValue = "";
 | |
|   this._hideSuggestions();
 | |
| 
 | |
|   this._getSearchEngines();
 | |
|   this._getStrings();
 | |
| }
 | |
| 
 | |
| ContentSearchUIController.prototype = {
 | |
| 
 | |
|   // The timeout (ms) of the remote suggestions.  Corresponds to
 | |
|   // SearchSuggestionController.remoteTimeout.  Uses
 | |
|   // SearchSuggestionController's default timeout if falsey.
 | |
|   remoteTimeout: undefined,
 | |
|   _oneOffButtons: [],
 | |
|   // Setting up the one off buttons causes an uninterruptible reflow. If we
 | |
|   // receive the list of engines while the newtab page is loading, this reflow
 | |
|   // may regress performance - so we set this flag and only set up the buttons
 | |
|   // if it's set when the suggestions table is actually opened.
 | |
|   _pendingOneOffRefresh: undefined,
 | |
| 
 | |
|   get defaultEngine() {
 | |
|     return this._defaultEngine;
 | |
|   },
 | |
| 
 | |
|   set defaultEngine(val) {
 | |
|     this._defaultEngine = val;
 | |
|     this._updateDefaultEngineHeader();
 | |
| 
 | |
|     if (val && document.activeElement == this.input) {
 | |
|       this._speculativeConnect();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   get engines() {
 | |
|     return this._engines;
 | |
|   },
 | |
| 
 | |
|   set engines(val) {
 | |
|     this._engines = val;
 | |
|     if (!this._table.hidden) {
 | |
|       this._setUpOneOffButtons();
 | |
|       return;
 | |
|     }
 | |
|     this._pendingOneOffRefresh = true;
 | |
|   },
 | |
| 
 | |
|   // The selectedIndex is the index of the element with the "selected" class in
 | |
|   // the list obtained by concatenating the suggestion rows, one-off buttons, and
 | |
|   // search settings button.
 | |
|   get selectedIndex() {
 | |
|     let allElts = [...this._suggestionsList.children,
 | |
|                    ...this._oneOffButtons,
 | |
|                    document.getElementById("contentSearchSettingsButton")];
 | |
|     for (let i = 0; i < allElts.length; ++i) {
 | |
|       let elt = allElts[i];
 | |
|       if (elt.classList.contains("selected")) {
 | |
|         return i;
 | |
|       }
 | |
|     }
 | |
|     return -1;
 | |
|   },
 | |
| 
 | |
|   set selectedIndex(idx) {
 | |
|     // Update the table's rows, and the input when there is a selection.
 | |
|     this._table.removeAttribute("aria-activedescendant");
 | |
|     this.input.removeAttribute("aria-activedescendant");
 | |
| 
 | |
|     let allElts = [...this._suggestionsList.children,
 | |
|                    ...this._oneOffButtons,
 | |
|                    document.getElementById("contentSearchSettingsButton")];
 | |
|     for (let i = 0; i < allElts.length; ++i) {
 | |
|       let elt = allElts[i];
 | |
|       let ariaSelectedElt = i < this.numSuggestions ? elt.firstChild : elt;
 | |
|       if (i == idx) {
 | |
|         elt.classList.add("selected");
 | |
|         ariaSelectedElt.setAttribute("aria-selected", "true");
 | |
|         this.input.setAttribute("aria-activedescendant", ariaSelectedElt.id);
 | |
|       }
 | |
|       else {
 | |
|         elt.classList.remove("selected");
 | |
|         ariaSelectedElt.setAttribute("aria-selected", "false");
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   get selectedEngineName() {
 | |
|     let selectedElt = this._table.querySelector(".selected");
 | |
|     if (selectedElt && selectedElt.engineName) {
 | |
|       return selectedElt.engineName;
 | |
|     }
 | |
|     return this.defaultEngine.name;
 | |
|   },
 | |
| 
 | |
|   get numSuggestions() {
 | |
|     return this._suggestionsList.children.length;
 | |
|   },
 | |
| 
 | |
|   selectAndUpdateInput: function (idx) {
 | |
|     this.selectedIndex = idx;
 | |
|     let newValue = this.suggestionAtIndex(idx) || this._stickyInputValue;
 | |
|     // Setting the input value when the value has not changed commits the current
 | |
|     // IME composition, which we don't want to do.
 | |
|     if (this.input.value != newValue) {
 | |
|       this.input.value = newValue;
 | |
|     }
 | |
|     this._updateSearchWithHeader();
 | |
|   },
 | |
| 
 | |
|   suggestionAtIndex: function (idx) {
 | |
|     let row = this._suggestionsList.children[idx];
 | |
|     return row ? row.textContent : null;
 | |
|   },
 | |
| 
 | |
|   deleteSuggestionAtIndex: function (idx) {
 | |
|     // Only form history suggestions can be deleted.
 | |
|     if (this.isFormHistorySuggestionAtIndex(idx)) {
 | |
|       let suggestionStr = this.suggestionAtIndex(idx);
 | |
|       this._sendMsg("RemoveFormHistoryEntry", suggestionStr);
 | |
|       this._suggestionsList.children[idx].remove();
 | |
|       this.selectAndUpdateInput(-1);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   isFormHistorySuggestionAtIndex: function (idx) {
 | |
|     let row = this._suggestionsList.children[idx];
 | |
|     return row && row.classList.contains("formHistory");
 | |
|   },
 | |
| 
 | |
|   addInputValueToFormHistory: function () {
 | |
|     this._sendMsg("AddFormHistoryEntry", this.input.value);
 | |
|   },
 | |
| 
 | |
|   handleEvent: function (event) {
 | |
|     this["_on" + event.type[0].toUpperCase() + event.type.substr(1)](event);
 | |
|   },
 | |
| 
 | |
|   _onCommand: function(aEvent) {
 | |
|     if (this.selectedIndex == this.numSuggestions + this._oneOffButtons.length) {
 | |
|       // Settings button was selected.
 | |
|       this._sendMsg("ManageEngines");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.search(aEvent);
 | |
| 
 | |
|     if (aEvent) {
 | |
|       aEvent.preventDefault();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   search: function (aEvent) {
 | |
|     if (!this.defaultEngine) {
 | |
|       return; // Not initialized yet.
 | |
|     }
 | |
| 
 | |
|     let searchText = this.input;
 | |
|     let searchTerms;
 | |
|     if (this._table.hidden ||
 | |
|         aEvent.originalTarget.id == "contentSearchDefaultEngineHeader") {
 | |
|       searchTerms = searchText.value;
 | |
|     }
 | |
|     else {
 | |
|       searchTerms = this.suggestionAtIndex(this.selectedIndex) || searchText.value;
 | |
|     }
 | |
|     // Send an event that will perform a search and Firefox Health Report will
 | |
|     // record that a search from the healthReportKey passed to the constructor.
 | |
|     let eventData = {
 | |
|       engineName: this.selectedEngineName,
 | |
|       searchString: searchTerms,
 | |
|       healthReportKey: this._healthReportKey,
 | |
|       searchPurpose: this._searchPurpose,
 | |
|       originalEvent: {
 | |
|         shiftKey: aEvent.shiftKey,
 | |
|         ctrlKey: aEvent.ctrlKey,
 | |
|         metaKey: aEvent.metaKey,
 | |
|         altKey: aEvent.altKey,
 | |
|         button: aEvent.button,
 | |
|       },
 | |
|     };
 | |
| 
 | |
|     if (this.suggestionAtIndex(this.selectedIndex)) {
 | |
|       eventData.selection = {
 | |
|         index: this.selectedIndex,
 | |
|         kind: aEvent instanceof MouseEvent ? "mouse" :
 | |
|               aEvent instanceof KeyboardEvent ? "key" : undefined,
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     this._sendMsg("Search", eventData);
 | |
|     this.addInputValueToFormHistory();
 | |
|   },
 | |
| 
 | |
|   _onInput: function () {
 | |
|     if (!this.input.value) {
 | |
|       this._stickyInputValue = "";
 | |
|       this._hideSuggestions();
 | |
|     }
 | |
|     else if (this.input.value != this._stickyInputValue) {
 | |
|       // Only fetch new suggestions if the input value has changed.
 | |
|       this._getSuggestions();
 | |
|       this.selectAndUpdateInput(-1);
 | |
|     }
 | |
|     this._updateSearchWithHeader();
 | |
|   },
 | |
| 
 | |
|   _onKeypress: function (event) {
 | |
|     let selectedIndexDelta = 0;
 | |
|     switch (event.keyCode) {
 | |
|     case event.DOM_VK_UP:
 | |
|       if (!this._table.hidden) {
 | |
|         selectedIndexDelta = -1;
 | |
|       }
 | |
|       break;
 | |
|     case event.DOM_VK_DOWN:
 | |
|       if (this._table.hidden) {
 | |
|         this._getSuggestions();
 | |
|       }
 | |
|       else {
 | |
|         selectedIndexDelta = 1;
 | |
|       }
 | |
|       break;
 | |
|     case event.DOM_VK_RIGHT:
 | |
|       // Allow normal caret movement until the caret is at the end of the input.
 | |
|       if (this.input.selectionStart != this.input.selectionEnd ||
 | |
|           this.input.selectionEnd != this.input.value.length) {
 | |
|         return;
 | |
|       }
 | |
|       if (this.numSuggestions && this.selectedIndex >= 0 &&
 | |
|           this.selectedIndex < this.numSuggestions) {
 | |
|         this.input.value = this.suggestionAtIndex(this.selectedIndex);
 | |
|         this.input.setAttribute("selection-index", this.selectedIndex);
 | |
|         this.input.setAttribute("selection-kind", "key");
 | |
|       } else {
 | |
|         // If we didn't select anything, make sure to remove the attributes
 | |
|         // in case they were populated last time.
 | |
|         this.input.removeAttribute("selection-index");
 | |
|         this.input.removeAttribute("selection-kind");
 | |
|       }
 | |
|       this._stickyInputValue = this.input.value;
 | |
|       this._hideSuggestions();
 | |
|       break;
 | |
|     case event.DOM_VK_RETURN:
 | |
|       this._onCommand(event);
 | |
|       break;
 | |
|     case event.DOM_VK_DELETE:
 | |
|       if (this.selectedIndex >= 0) {
 | |
|         this.deleteSuggestionAtIndex(this.selectedIndex);
 | |
|       }
 | |
|       break;
 | |
|     case event.DOM_VK_ESCAPE:
 | |
|       if (!this._table.hidden) {
 | |
|         this._hideSuggestions();
 | |
|       }
 | |
|     default:
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (selectedIndexDelta) {
 | |
|       // Update the selection.
 | |
|       let newSelectedIndex = this.selectedIndex + selectedIndexDelta;
 | |
|       if (newSelectedIndex < -1) {
 | |
|         newSelectedIndex = this.numSuggestions + this._oneOffButtons.length;
 | |
|       }
 | |
|       else if (this.numSuggestions + this._oneOffButtons.length < newSelectedIndex) {
 | |
|         newSelectedIndex = -1;
 | |
|       }
 | |
|       this.selectAndUpdateInput(newSelectedIndex);
 | |
| 
 | |
|       // Prevent the input's caret from moving.
 | |
|       event.preventDefault();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _onFocus: function () {
 | |
|     if (this._mousedown) {
 | |
|       return;
 | |
|     }
 | |
|     // When the input box loses focus to something in our table, we refocus it
 | |
|     // immediately. This causes the focus highlight to flicker, so we set a
 | |
|     // custom attribute which consumers should use for focus highlighting. This
 | |
|     // attribute is removed only when we do not immediately refocus the input
 | |
|     // box, thus eliminating flicker.
 | |
|     this.input.setAttribute("keepfocus", "true");
 | |
|     this._speculativeConnect();
 | |
|   },
 | |
| 
 | |
|   _onBlur: function () {
 | |
|     if (this._mousedown) {
 | |
|       // At this point, this.input has lost focus, but a new element has not yet
 | |
|       // received it. If we re-focus this.input directly, the new element will
 | |
|       // steal focus immediately, so we queue it instead.
 | |
|       setTimeout(() => this.input.focus(), 0);
 | |
|       return;
 | |
|     }
 | |
|     this.input.removeAttribute("keepfocus");
 | |
|     this._hideSuggestions();
 | |
|   },
 | |
| 
 | |
|   _onMousemove: function (event) {
 | |
|     this.selectedIndex = this._indexOfTableItem(event.target);
 | |
|   },
 | |
| 
 | |
|   _onMouseup: function (event) {
 | |
|     if (event.button == 2) {
 | |
|       return;
 | |
|     }
 | |
|     this._onCommand(event);
 | |
|   },
 | |
| 
 | |
|   _onClick: function (event) {
 | |
|     this._onMouseup(event);
 | |
|   },
 | |
| 
 | |
|   _onContentSearchService: function (event) {
 | |
|     let methodName = "_onMsg" + event.detail.type;
 | |
|     if (methodName in this) {
 | |
|       this[methodName](event.detail.data);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _onMsgFocusInput: function (event) {
 | |
|     this.input.focus();
 | |
|   },
 | |
| 
 | |
|   _onMsgSuggestions: function (suggestions) {
 | |
|     // Ignore the suggestions if their search string or engine doesn't match
 | |
|     // ours.  Due to the async nature of message passing, this can easily happen
 | |
|     // when the user types quickly.
 | |
|     if (this._stickyInputValue != suggestions.searchString ||
 | |
|         this.defaultEngine.name != suggestions.engineName) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this._clearSuggestionRows();
 | |
| 
 | |
|     // Position and size the table.
 | |
|     let { left } = this.input.getBoundingClientRect();
 | |
|     this._table.style.top = this.input.offsetHeight + "px";
 | |
|     this._table.style.minWidth = this.input.offsetWidth + "px";
 | |
|     this._table.style.maxWidth = (window.innerWidth - left - 40) + "px";
 | |
| 
 | |
|     // Add the suggestions to the table.
 | |
|     let searchWords =
 | |
|       new Set(suggestions.searchString.trim().toLowerCase().split(/\s+/));
 | |
|     for (let i = 0; i < MAX_DISPLAYED_SUGGESTIONS; i++) {
 | |
|       let type, idx;
 | |
|       if (i < suggestions.formHistory.length) {
 | |
|         [type, idx] = ["formHistory", i];
 | |
|       }
 | |
|       else {
 | |
|         let j = i - suggestions.formHistory.length;
 | |
|         if (j < suggestions.remote.length) {
 | |
|           [type, idx] = ["remote", j];
 | |
|         }
 | |
|         else {
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
|       this._suggestionsList.appendChild(
 | |
|         this._makeTableRow(type, suggestions[type][idx], i, searchWords));
 | |
|     }
 | |
| 
 | |
|     if (this._table.hidden) {
 | |
|       this.selectedIndex = -1;
 | |
|       if (this._pendingOneOffRefresh) {
 | |
|         this._setUpOneOffButtons();
 | |
|         delete this._pendingOneOffRefresh;
 | |
|       }
 | |
|       this._table.hidden = false;
 | |
|       this.input.setAttribute("aria-expanded", "true");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _onMsgState: function (state) {
 | |
|     this.defaultEngine = {
 | |
|       name: state.currentEngine.name,
 | |
|       icon: this._getFaviconURIFromBuffer(state.currentEngine.iconBuffer),
 | |
|     };
 | |
|     this.engines = state.engines;
 | |
|   },
 | |
| 
 | |
|   _onMsgCurrentState: function (state) {
 | |
|     this._onMsgState(state);
 | |
|   },
 | |
| 
 | |
|   _onMsgCurrentEngine: function (engine) {
 | |
|     this.defaultEngine = {
 | |
|       name: engine.name,
 | |
|       icon: this._getFaviconURIFromBuffer(engine.iconBuffer),
 | |
|     };
 | |
|     if (!this._table.hidden) {
 | |
|       this._setUpOneOffButtons();
 | |
|       return;
 | |
|     }
 | |
|     this._pendingOneOffRefresh = true;
 | |
|   },
 | |
| 
 | |
|   _onMsgStrings: function (strings) {
 | |
|     this._strings = strings;
 | |
|     this._updateDefaultEngineHeader();
 | |
|     this._updateSearchWithHeader();
 | |
|     document.getElementById("contentSearchSettingsButton").textContent =
 | |
|       this._strings.searchSettings;
 | |
|   },
 | |
| 
 | |
|   _updateDefaultEngineHeader: function () {
 | |
|     let header = document.getElementById("contentSearchDefaultEngineHeader");
 | |
|     if (this.defaultEngine.icon) {
 | |
|       header.firstChild.setAttribute("src", this.defaultEngine.icon);
 | |
|     }
 | |
|     if (!this._strings) {
 | |
|       return;
 | |
|     }
 | |
|     while (header.firstChild.nextSibling) {
 | |
|       header.firstChild.nextSibling.remove();
 | |
|     }
 | |
|     header.appendChild(document.createTextNode(
 | |
|       this._strings.searchHeader.replace("%S", this.defaultEngine.name)));
 | |
|   },
 | |
| 
 | |
|   _updateSearchWithHeader: function () {
 | |
|     if (!this._strings) {
 | |
|       return;
 | |
|     }
 | |
|     let searchWithHeader = document.getElementById("contentSearchSearchWithHeader");
 | |
|     while (searchWithHeader.firstChild) {
 | |
|       searchWithHeader.firstChild.remove();
 | |
|     }
 | |
|     if (this.input.value) {
 | |
|       let html = "<span class='contentSearchSearchWithHeaderSearchText'>" +
 | |
|                  this.input.value + "</span>";
 | |
|       html = this._strings.searchForKeywordsWith.replace("%S", html);
 | |
|       searchWithHeader.innerHTML = html;
 | |
|       return;
 | |
|     }
 | |
|     searchWithHeader.appendChild(document.createTextNode(this._strings.searchWithHeader));
 | |
|   },
 | |
| 
 | |
|   _speculativeConnect: function () {
 | |
|     if (this.defaultEngine) {
 | |
|       this._sendMsg("SpeculativeConnect", this.defaultEngine.name);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _makeTableRow: function (type, suggestionStr, currentRow, searchWords) {
 | |
|     let row = document.createElementNS(HTML_NS, "tr");
 | |
|     row.dir = "auto";
 | |
|     row.classList.add("contentSearchSuggestionRow");
 | |
|     row.classList.add(type);
 | |
|     row.setAttribute("role", "presentation");
 | |
|     row.addEventListener("mousemove", this);
 | |
|     row.addEventListener("mouseup", this);
 | |
| 
 | |
|     let entry = document.createElementNS(HTML_NS, "td");
 | |
|     let img = document.createElementNS(HTML_NS, "div");
 | |
|     img.setAttribute("class", "historyIcon");
 | |
|     entry.appendChild(img);
 | |
|     entry.classList.add("contentSearchSuggestionEntry");
 | |
|     entry.setAttribute("role", "option");
 | |
|     entry.id = this._idPrefix + SUGGESTION_ID_PREFIX + currentRow;
 | |
|     entry.setAttribute("aria-selected", "false");
 | |
| 
 | |
|     let suggestionWords = suggestionStr.trim().toLowerCase().split(/\s+/);
 | |
|     for (let i = 0; i < suggestionWords.length; i++) {
 | |
|       let word = suggestionWords[i];
 | |
|       let wordSpan = document.createElementNS(HTML_NS, "span");
 | |
|       if (searchWords.has(word)) {
 | |
|         wordSpan.classList.add("typed");
 | |
|       }
 | |
|       wordSpan.textContent = word;
 | |
|       entry.appendChild(wordSpan);
 | |
|       if (i < suggestionWords.length - 1) {
 | |
|         entry.appendChild(document.createTextNode(" "));
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     row.appendChild(entry);
 | |
|     return row;
 | |
|   },
 | |
| 
 | |
|   // Converts favicon array buffer into data URI of the right size and dpi.
 | |
|   _getFaviconURIFromBuffer: function (buffer) {
 | |
|     let blob = new Blob([buffer]);
 | |
|     let dpiSize = Math.round(16 * window.devicePixelRatio);
 | |
|     let sizeStr = dpiSize + "," + dpiSize;
 | |
|     return URL.createObjectURL(blob) + "#-moz-resolution=" + sizeStr;
 | |
|   },
 | |
| 
 | |
|   _getSearchEngines: function () {
 | |
|     this._sendMsg("GetState");
 | |
|   },
 | |
| 
 | |
|   _getStrings: function () {
 | |
|     this._sendMsg("GetStrings");
 | |
|   },
 | |
| 
 | |
|   _getSuggestions: function () {
 | |
|     this._stickyInputValue = this.input.value;
 | |
|     if (this.defaultEngine) {
 | |
|       this._sendMsg("GetSuggestions", {
 | |
|         engineName: this.defaultEngine.name,
 | |
|         searchString: this.input.value,
 | |
|         remoteTimeout: this.remoteTimeout,
 | |
|       });
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _clearSuggestionRows: function() {
 | |
|     while (this._suggestionsList.firstElementChild) {
 | |
|       this._suggestionsList.firstElementChild.remove();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _hideSuggestions: function () {
 | |
|     this.input.setAttribute("aria-expanded", "false");
 | |
|     this._table.hidden = true;
 | |
|   },
 | |
| 
 | |
|   _indexOfTableItem: function (elt) {
 | |
|     if (elt.classList.contains("contentSearchOneOffItem")) {
 | |
|       return this.numSuggestions + this._oneOffButtons.indexOf(elt);
 | |
|     }
 | |
|     if (elt.classList.contains("contentSearchSettingsButton")) {
 | |
|       return this.numSuggestions + this._oneOffButtons.length;
 | |
|     }
 | |
|     while (elt && elt.localName != "tr") {
 | |
|       elt = elt.parentNode;
 | |
|     }
 | |
|     if (!elt) {
 | |
|       throw new Error("Element is not a row");
 | |
|     }
 | |
|     return elt.rowIndex;
 | |
|   },
 | |
| 
 | |
|   _makeTable: function (id) {
 | |
|     this._table = document.createElementNS(HTML_NS, "table");
 | |
|     this._table.id = id;
 | |
|     this._table.hidden = true;
 | |
|     this._table.classList.add("contentSearchSuggestionTable");
 | |
|     this._table.setAttribute("role", "presentation");
 | |
| 
 | |
|     // When the search input box loses focus, we want to immediately give focus
 | |
|     // back to it if the blur was because the user clicked somewhere in the table.
 | |
|     // onBlur uses the _mousedown flag to detect this.
 | |
|     this._table.addEventListener("mousedown", () => { this._mousedown = true; });
 | |
|     document.addEventListener("mouseup", () => { delete this._mousedown; });
 | |
| 
 | |
|     // Deselect the selected element on mouseout if it wasn't a suggestion.
 | |
|     this._table.addEventListener("mouseout", () => {
 | |
|       if (this.selectedIndex >= this.numSuggestions) {
 | |
|         this.selectAndUpdateInput(-1);
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     // If a search is loaded in the same tab, ensure the suggestions dropdown
 | |
|     // is hidden immediately when the page starts loading and not when it first
 | |
|     // appears, in order to provide timely feedback to the user.
 | |
|     window.addEventListener("beforeunload", () => { this._hideSuggestions(); });
 | |
| 
 | |
|     let headerRow = document.createElementNS(HTML_NS, "tr");
 | |
|     let header = document.createElementNS(HTML_NS, "td");
 | |
|     headerRow.setAttribute("class", "contentSearchHeaderRow");
 | |
|     header.setAttribute("class", "contentSearchHeader");
 | |
|     let img = document.createElementNS(HTML_NS, "img");
 | |
|     img.setAttribute("src", "chrome://browser/skin/search-engine-placeholder.png");
 | |
|     header.appendChild(img);
 | |
|     header.id = "contentSearchDefaultEngineHeader";
 | |
|     headerRow.appendChild(header);
 | |
|     headerRow.addEventListener("click", this);
 | |
|     this._table.appendChild(headerRow);
 | |
| 
 | |
|     let row = document.createElementNS(HTML_NS, "tr");
 | |
|     row.setAttribute("class", "contentSearchSuggestionsContainer");
 | |
|     let cell = document.createElementNS(HTML_NS, "td");
 | |
|     cell.setAttribute("class", "contentSearchSuggestionsContainer");
 | |
|     this._suggestionsList = document.createElementNS(HTML_NS, "table");
 | |
|     this._suggestionsList.setAttribute("class", "contentSearchSuggestionsList");
 | |
|     cell.appendChild(this._suggestionsList);
 | |
|     row.appendChild(cell);
 | |
|     this._table.appendChild(row);
 | |
|     this._suggestionsList.setAttribute("role", "listbox");
 | |
| 
 | |
|     this._oneOffsTable = document.createElementNS(HTML_NS, "table");
 | |
|     this._oneOffsTable.setAttribute("class", "contentSearchOneOffsTable");
 | |
|     this._oneOffsTable.classList.add("contentSearchSuggestionsContainer");
 | |
|     this._oneOffsTable.setAttribute("role", "group");
 | |
|     this._table.appendChild(this._oneOffsTable);
 | |
| 
 | |
|     headerRow = document.createElementNS(HTML_NS, "tr");
 | |
|     header = document.createElementNS(HTML_NS, "td");
 | |
|     headerRow.setAttribute("class", "contentSearchHeaderRow");
 | |
|     header.setAttribute("class", "contentSearchHeader");
 | |
|     headerRow.appendChild(header);
 | |
|     header.id = "contentSearchSearchWithHeader";
 | |
|     this._oneOffsTable.appendChild(headerRow);
 | |
| 
 | |
|     let button = document.createElementNS(HTML_NS, "button");
 | |
|     button.setAttribute("class", "contentSearchSettingsButton");
 | |
|     button.classList.add("contentSearchHeaderRow");
 | |
|     button.classList.add("contentSearchHeader");
 | |
|     button.id = "contentSearchSettingsButton";
 | |
|     button.addEventListener("click", this);
 | |
|     button.addEventListener("mousemove", this);
 | |
|     this._table.appendChild(button);
 | |
| 
 | |
|     return this._table;
 | |
|   },
 | |
| 
 | |
|   _setUpOneOffButtons: function () {
 | |
|     // Sometimes we receive a CurrentEngine message from the ContentSearch service
 | |
|     // before we've received a State message - i.e. before we have our engines.
 | |
|     if (!this._engines) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     while (this._oneOffsTable.firstChild.nextSibling) {
 | |
|       this._oneOffsTable.firstChild.nextSibling.remove();
 | |
|     }
 | |
| 
 | |
|     this._oneOffButtons = [];
 | |
| 
 | |
|     let engines = this._engines.filter(aEngine => aEngine.name != this.defaultEngine.name);
 | |
|     if (!engines.length) {
 | |
|       this._oneOffsTable.hidden = true;
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const kDefaultButtonWidth = 49; // 48px + 1px border.
 | |
|     let rowWidth = this.input.offsetWidth - 2; // 2px border.
 | |
|     let enginesPerRow = Math.floor(rowWidth / kDefaultButtonWidth);
 | |
|     let buttonWidth = Math.floor(rowWidth / enginesPerRow);
 | |
| 
 | |
|     let row = document.createElementNS(HTML_NS, "tr");
 | |
|     let cell = document.createElementNS(HTML_NS, "td");
 | |
|     row.setAttribute("class", "contentSearchSuggestionsContainer");
 | |
|     cell.setAttribute("class", "contentSearchSuggestionsContainer");
 | |
| 
 | |
|     for (let i = 0; i < engines.length; ++i) {
 | |
|       let engine = engines[i];
 | |
|       if (i > 0 && i % enginesPerRow == 0) {
 | |
|         row.appendChild(cell);
 | |
|         this._oneOffsTable.appendChild(row);
 | |
|         row = document.createElementNS(HTML_NS, "tr");
 | |
|         cell = document.createElementNS(HTML_NS, "td");
 | |
|         row.setAttribute("class", "contentSearchSuggestionsContainer");
 | |
|         cell.setAttribute("class", "contentSearchSuggestionsContainer");
 | |
|       }
 | |
|       let button = document.createElementNS(HTML_NS, "button");
 | |
|       button.setAttribute("class", "contentSearchOneOffItem");
 | |
|       let img = document.createElementNS(HTML_NS, "img");
 | |
|       let uri = "chrome://browser/skin/search-engine-placeholder.png";
 | |
|       if (engine.iconBuffer) {
 | |
|         uri = this._getFaviconURIFromBuffer(engine.iconBuffer);
 | |
|       }
 | |
|       img.setAttribute("src", uri);
 | |
|       button.appendChild(img);
 | |
|       button.style.width = buttonWidth + "px";
 | |
|       button.setAttribute("title", engine.name);
 | |
| 
 | |
|       button.engineName = engine.name;
 | |
|       button.addEventListener("click", this);
 | |
|       button.addEventListener("mousemove", this);
 | |
| 
 | |
|       if (engines.length - i <= enginesPerRow - (i % enginesPerRow)) {
 | |
|         button.classList.add("last-row");
 | |
|       }
 | |
| 
 | |
|       if ((i + 1) % enginesPerRow == 0) {
 | |
|         button.classList.add("end-of-row");
 | |
|       }
 | |
| 
 | |
|       button.id = ONE_OFF_ID_PREFIX + i;
 | |
|       cell.appendChild(button);
 | |
|       this._oneOffButtons.push(button);
 | |
|     }
 | |
|     row.appendChild(cell);
 | |
|     this._oneOffsTable.appendChild(row);
 | |
|     this._oneOffsTable.hidden = false;
 | |
|   },
 | |
| 
 | |
|   _sendMsg: function (type, data=null) {
 | |
|     dispatchEvent(new CustomEvent("ContentSearchClient", {
 | |
|       detail: {
 | |
|         type: type,
 | |
|         data: data,
 | |
|       },
 | |
|     }));
 | |
|   },
 | |
| };
 | |
| 
 | |
| return ContentSearchUIController;
 | |
| })();
 | 
