Bug 1523602 - Re-use existing rows when receiving results. r=mak
☠☠ backed out by 9f3caf4a14b0 ☠ ☠
authorDão Gottwald <dao@mozilla.com>
Thu, 28 Mar 2019 12:59:57 +0000
changeset 466576 6385899fe7b9ccd58f9ea0091e9d158416f83605
parent 466575 a597a0295d90209160e08dfa6d715e39f63415f0
child 466577 88ab548583986f0f1700f18bc0ebce764d581a66
push id81677
push userdgottwald@mozilla.com
push dateThu, 28 Mar 2019 13:15:39 +0000
treeherderautoland@6385899fe7b9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1523602
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1523602 - Re-use existing rows when receiving results. r=mak Differential Revision: https://phabricator.services.mozilla.com/D25047
browser/components/urlbar/UrlbarView.jsm
browser/themes/shared/urlbar-autocomplete.inc.css
--- a/browser/components/urlbar/UrlbarView.jsm
+++ b/browser/components/urlbar/UrlbarView.jsm
@@ -199,58 +199,54 @@ class UrlbarView {
 
   onQueryFinished(queryContext) {
     this._rows.style.minHeight = "";
   }
 
   onQueryResults(queryContext) {
     this._queryContext = queryContext;
 
-    let fragment = this.document.createDocumentFragment();
-    for (let resultIndex in queryContext.results) {
-      fragment.appendChild(this._createRow(resultIndex));
+    let resultIndex = 0;
+    for (let row of this._rows.children) {
+      if (resultIndex < queryContext.results.length) {
+        this._updateRow(row, resultIndex);
+      } else {
+        row.remove();
+      }
+      resultIndex++;
+    }
+    for (; resultIndex < queryContext.results.length; resultIndex++) {
+      let row = this._createRow();
+      this._updateRow(row, resultIndex);
+      this._rows.appendChild(row);
     }
 
     let isFirstPreselectedResult = false;
     if (queryContext.lastResultCount == 0) {
       if (queryContext.preselected) {
         isFirstPreselectedResult = true;
-        this._selectItem(fragment.firstElementChild, {
+        this._selectItem(this._rows.firstElementChild, {
           updateInput: false,
           setAccessibleFocus: false,
         });
       } else {
         // Clear the selection when we get a new set of results.
         this._selectItem(null);
       }
       // Hide the one-off search buttons if the input starts with a potential @
       // search alias or the search restriction character.
       let trimmedValue = this.input.textValue.trim();
       this._enableOrDisableOneOffSearches(
         !trimmedValue ||
         (trimmedValue[0] != "@" &&
          (trimmedValue[0] != UrlbarTokenizer.RESTRICT.SEARCH ||
           trimmedValue.length != 1))
       );
-    } else if (this._selected) {
-      // Ensure the selection is stable.
-      // TODO bug 1523602: the selection should stay on the node that had it, if
-      // it's still in the current result set.
-      let resultIndex = this._selected.getAttribute("resultIndex");
-      this._selectItem(fragment.children[resultIndex], {
-        updateInput: false,
-        setAccessibleFocus: false,
-      });
     }
 
-    // TODO bug 1523602: For now, clear the results for each set received.
-    // We should be updating the existing list instead.
-    this._rows.textContent = "";
-    this._rows.appendChild(fragment);
-
     this._openPanel();
 
     if (isFirstPreselectedResult) {
       // The first, preselected result may be a search alias result, so apply
       // formatting if necessary.  Conversely, the first result of the previous
       // query may have been an alias, so remove formatting if necessary.
       this.input.formatValue();
     }
@@ -389,126 +385,143 @@ class UrlbarView {
       contentWidth -= start + endOffset;
     } else {
       this.panel.style.removeProperty("--item-padding-start");
       this.panel.style.removeProperty("--item-padding-end");
     }
     this.panel.style.setProperty("--item-content-width", Math.round(contentWidth) + "px");
   }
 
-  _createRow(resultIndex) {
-    let result = this._queryContext.results[resultIndex];
+  _createRow() {
     let item = this._createElement("div");
-    item.id = "urlbarView-row-" + resultIndex;
     item.className = "urlbarView-row";
-    item.setAttribute("resultIndex", resultIndex);
     item.setAttribute("role", "option");
-
-    if (result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
-        !result.payload.isKeywordOffer) {
-      item.setAttribute("type", "search");
-    } else if (result.type == UrlbarUtils.RESULT_TYPE.REMOTE_TAB) {
-      item.setAttribute("type", "remotetab");
-    } else if (result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH) {
-      item.setAttribute("type", "switchtab");
-    } else if (result.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
-      item.setAttribute("type", "bookmark");
-    }
+    item._elements = new Map;
 
     let content = this._createElement("span");
     content.className = "urlbarView-row-inner";
     item.appendChild(content);
 
     let typeIcon = this._createElement("span");
     typeIcon.className = "urlbarView-type-icon";
     content.appendChild(typeIcon);
 
     let favicon = this._createElement("img");
     favicon.className = "urlbarView-favicon";
+    content.appendChild(favicon);
+    item._elements.set("favicon", favicon);
+
+    let title = this._createElement("span");
+    title.className = "urlbarView-title";
+    content.appendChild(title);
+    item._elements.set("title", title);
+
+    let tagsContainer = this._createElement("div");
+    tagsContainer.className = "urlbarView-tags";
+    content.appendChild(tagsContainer);
+    item._elements.set("tagsContainer", tagsContainer);
+
+    let titleSeparator = this._createElement("span");
+    titleSeparator.className = "urlbarView-title-separator";
+    content.appendChild(titleSeparator);
+
+    let action = this._createElement("span");
+    action.className = "urlbarView-secondary urlbarView-action";
+    content.appendChild(action);
+    item._elements.set("action", action);
+
+    let url = this._createElement("span");
+    url.className = "urlbarView-secondary urlbarView-url";
+    content.appendChild(url);
+    item._elements.set("url", url);
+
+    return item;
+  }
+
+  _updateRow(item, resultIndex) {
+    let result = this._queryContext.results[resultIndex];
+    item.id = "urlbarView-row-" + resultIndex;
+    item.setAttribute("resultIndex", resultIndex);
+
+    if (result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+        !result.payload.isKeywordOffer) {
+      item.setAttribute("type", "search");
+    } else if (result.type == UrlbarUtils.RESULT_TYPE.REMOTE_TAB) {
+      item.setAttribute("type", "remotetab");
+    } else if (result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH) {
+      item.setAttribute("type", "switchtab");
+    } else if (result.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
+      item.setAttribute("type", "bookmark");
+    } else {
+      item.removeAttribute("type");
+    }
+
+    let favicon = item._elements.get("favicon");
     if (result.type == UrlbarUtils.RESULT_TYPE.SEARCH ||
         result.type == UrlbarUtils.RESULT_TYPE.KEYWORD) {
       favicon.src = result.payload.icon || UrlbarUtils.ICON.SEARCH_GLASS;
     } else {
       favicon.src = result.payload.icon || UrlbarUtils.ICON.DEFAULT;
     }
-    content.appendChild(favicon);
 
-    let title = this._createElement("span");
-    title.className = "urlbarView-title";
     this._addTextContentWithHighlights(
-      title, result.title, result.titleHighlights);
-    content.appendChild(title);
+      item._elements.get("title"), result.title, result.titleHighlights);
 
+    let tagsContainer = item._elements.get("tagsContainer");
     if (result.payload.tags && result.payload.tags.length > 0) {
-      const tagsContainer = this._createElement("div");
-      tagsContainer.className = "urlbarView-tags";
       tagsContainer.append(...result.payload.tags.map((tag, i) => {
         const element = this._createElement("span");
         element.className = "urlbarView-tag";
         this._addTextContentWithHighlights(
           element, tag, result.payloadHighlights.tags[i]);
         return element;
       }));
-      content.appendChild(tagsContainer);
+    } else {
+      tagsContainer.textContent = "";
     }
 
-    let titleSeparator = this._createElement("span");
-    titleSeparator.className = "urlbarView-title-separator";
-    content.appendChild(titleSeparator);
-
-    let action;
-    let url;
-    let setAction = text => {
-      action = this._createElement("span");
-      action.className = "urlbarView-secondary urlbarView-action";
-      action.textContent = text;
-    };
-    let setURL = () => {
-      url = this._createElement("span");
-      url.className = "urlbarView-secondary urlbarView-url";
-      this._addTextContentWithHighlights(url, result.payload.displayUrl,
-                                         result.payloadHighlights.displayUrl || []);
-    };
+    let action = "";
+    let setURL = false;
     switch (result.type) {
       case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
-        setAction(bundle.GetStringFromName("switchToTab2"));
-        setURL();
+        action = bundle.GetStringFromName("switchToTab2");
+        setURL = true;
         break;
       case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
-        setAction(result.payload.device);
-        setURL();
+        action = result.payload.device;
+        setURL = true;
         break;
       case UrlbarUtils.RESULT_TYPE.SEARCH:
-        setAction(bundle.formatStringFromName("searchWithEngine",
-                                              [result.payload.engine], 1));
+        action = bundle.formatStringFromName("searchWithEngine",
+                                             [result.payload.engine], 1);
         break;
       case UrlbarUtils.RESULT_TYPE.KEYWORD:
         if (result.payload.input.trim() == result.payload.keyword) {
-          setAction(bundle.GetStringFromName("visit"));
+          action = bundle.GetStringFromName("visit");
         }
         break;
       case UrlbarUtils.RESULT_TYPE.OMNIBOX:
-        setAction(result.payload.content);
+        action = result.payload.content;
         break;
       default:
         if (result.heuristic) {
-          setAction(bundle.GetStringFromName("visit"));
+          action = bundle.GetStringFromName("visit");
         } else {
-          setURL();
+          setURL = true;
         }
         break;
     }
-    if (action) {
-      content.appendChild(action);
+    let url = item._elements.get("url");
+    if (setURL) {
+      this._addTextContentWithHighlights(url, result.payload.displayUrl,
+                                         result.payloadHighlights.displayUrl || []);
+    } else {
+      url.textContent = "";
     }
-    if (url) {
-      content.appendChild(url);
-    }
-
-    return item;
+    item._elements.get("action").textContent = action;
   }
 
   _selectItem(item, {
     updateInput = true,
     setAccessibleFocus = true,
   } = {}) {
     if (this._selected) {
       this._selected.toggleAttribute("selected", false);
@@ -546,16 +559,17 @@ class UrlbarView {
    * @param {Node} parentNode
    *   The text content will be added to this node.
    * @param {string} textContent
    *   The text content to give the node.
    * @param {array} highlights
    *   The matches to highlight in the text.
    */
   _addTextContentWithHighlights(parentNode, textContent, highlights) {
+    parentNode.textContent = "";
     if (!textContent) {
       return;
     }
     highlights = (highlights || []).concat([[textContent.length, 0]]);
     let index = 0;
     for (let [highlightIndex, highlightLength] of highlights) {
       if (highlightIndex - index > 0) {
         parentNode.appendChild(
--- a/browser/themes/shared/urlbar-autocomplete.inc.css
+++ b/browser/themes/shared/urlbar-autocomplete.inc.css
@@ -121,17 +121,17 @@
 }
 
 .urlbarView-title-separator::before {
   content: "\2014";
   color: var(--panel-disabled-color);
   margin: 0 .4em;
 }
 
-.urlbarView-title:empty + .urlbarView-title-separator {
+.urlbarView-title:empty + .urlbarView-tags:empty + .urlbarView-title-separator {
   display: none;
 }
 
 .urlbarView-tags,
 .urlbarView-secondary {
   font-size: .85em;
 }