summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/db/gloda/content
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/db/gloda/content')
-rw-r--r--comm/mailnews/db/gloda/content/autocomplete-richlistitem.js644
-rw-r--r--comm/mailnews/db/gloda/content/glodacomplete.js466
2 files changed, 1110 insertions, 0 deletions
diff --git a/comm/mailnews/db/gloda/content/autocomplete-richlistitem.js b/comm/mailnews/db/gloda/content/autocomplete-richlistitem.js
new file mode 100644
index 0000000000..916c6ef5d5
--- /dev/null
+++ b/comm/mailnews/db/gloda/content/autocomplete-richlistitem.js
@@ -0,0 +1,644 @@
+/* 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";
+
+/* global MozXULElement, MozElements */
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ const gGlodaCompleteStrings = Services.strings.createBundle(
+ "chrome://messenger/locale/glodaComplete.properties"
+ );
+
+ /**
+ * The MozGlodacompleteBaseRichlistitem widget is the
+ * abstract base class for all the gloda autocomplete items.
+ *
+ * @abstract
+ * @augments {MozElements.MozRichlistitem}
+ */
+ class MozGlodacompleteBaseRichlistitem extends MozElements.MozRichlistitem {
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+ this._boundaryCutoff = null;
+ }
+
+ get boundaryCutoff() {
+ if (!this._boundaryCutoff) {
+ this._boundaryCutoff = Services.prefs.getIntPref(
+ "toolkit.autocomplete.richBoundaryCutoff"
+ );
+ }
+ return this._boundaryCutoff;
+ }
+
+ _getBoundaryIndices(aText, aSearchTokens) {
+ // Short circuit for empty search ([""] == "")
+ if (aSearchTokens == "") {
+ return [0, aText.length];
+ }
+
+ // Find which regions of text match the search terms.
+ let regions = [];
+ for (let search of aSearchTokens) {
+ let matchIndex;
+ let startIndex = 0;
+ let searchLen = search.length;
+
+ // Find all matches of the search terms, but stop early for perf.
+ let lowerText = aText.toLowerCase().substr(0, this.boundaryCutoff);
+ while ((matchIndex = lowerText.indexOf(search, startIndex)) >= 0) {
+ // Start the next search from where this one finished.
+ startIndex = matchIndex + searchLen;
+ regions.push([matchIndex, startIndex]);
+ }
+ }
+
+ // Sort the regions by start position then end position.
+ regions = regions.sort(function (a, b) {
+ let start = a[0] - b[0];
+ return start == 0 ? a[1] - b[1] : start;
+ });
+
+ // Generate the boundary indices from each region.
+ let start = 0;
+ let end = 0;
+ let boundaries = [];
+ for (let i = 0; i < regions.length; i++) {
+ // We have a new boundary if the start of the next is past the end.
+ let region = regions[i];
+ if (region[0] > end) {
+ // First index is the beginning of match.
+ boundaries.push(start);
+ // Second index is the beginning of non-match.
+ boundaries.push(end);
+
+ // Track the new region now that we've stored the previous one.
+ start = region[0];
+ }
+
+ // Push back the end index for the current or new region.
+ end = Math.max(end, region[1]);
+ }
+
+ // Add the last region.
+ boundaries.push(start);
+ boundaries.push(end);
+
+ // Put on the end boundary if necessary.
+ if (end < aText.length) {
+ boundaries.push(aText.length);
+ }
+
+ // Skip the first item because it's always 0.
+ return boundaries.slice(1);
+ }
+
+ _getSearchTokens(aSearch) {
+ let search = aSearch.toLowerCase();
+ return search.split(/\s+/);
+ }
+
+ _needsAlternateEmphasis(aText) {
+ for (let i = aText.length - 1; i >= 0; i--) {
+ let charCode = aText.charCodeAt(i);
+ // Arabic, Syriac, Indic languages are likely to have ligatures
+ // that are broken when using the main emphasis styling.
+ if (0x0600 <= charCode && charCode <= 0x109f) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ _setUpDescription(aDescriptionElement, aText) {
+ // Get rid of all previous text.
+ while (aDescriptionElement.hasChildNodes()) {
+ aDescriptionElement.lastChild.remove();
+ }
+
+ // Get the indices that separate match and non-match text.
+ let search = this.getAttribute("text");
+ let tokens = this._getSearchTokens(search);
+ let indices = this._getBoundaryIndices(aText, tokens);
+
+ // If we're searching for something that needs alternate emphasis,
+ // we'll need to check the text that we match.
+ let checkAlt = this._needsAlternateEmphasis(search);
+
+ let next;
+ let start = 0;
+ let len = indices.length;
+ // Even indexed boundaries are matches, so skip the 0th if it's empty.
+ for (let i = indices[0] == 0 ? 1 : 0; i < len; i++) {
+ next = indices[i];
+ let text = aText.substr(start, next - start);
+ start = next;
+
+ if (i % 2 == 0) {
+ // Emphasize the text for even indices
+ let span = aDescriptionElement.appendChild(
+ document.createElementNS("http://www.w3.org/1999/xhtml", "span")
+ );
+ span.className =
+ checkAlt && this._needsAlternateEmphasis(text)
+ ? "ac-emphasize-alt"
+ : "ac-emphasize-text";
+ span.textContent = text;
+ } else {
+ // Otherwise, it's plain text
+ aDescriptionElement.appendChild(document.createTextNode(text));
+ }
+ }
+ }
+
+ _setUpOverflow(aParentBox, aEllipsis) {
+ // Hide the ellipsis in case there's just enough to not underflow.
+ aEllipsis.hidden = true;
+
+ // Start with the parent's width and subtract off its children.
+ let tooltip = [];
+ let children = aParentBox.children;
+ let widthDiff = aParentBox.getBoundingClientRect().width;
+
+ for (let i = 0; i < children.length; i++) {
+ // Only consider a child if it actually takes up space.
+ let childWidth = children[i].getBoundingClientRect().width;
+ if (childWidth > 0) {
+ // Subtract a little less to account for subpixel rounding.
+ widthDiff -= childWidth - 0.5;
+
+ // Add to the tooltip if it's not hidden and has text.
+ let childText = children[i].textContent;
+ if (childText) {
+ tooltip.push(childText);
+ }
+ }
+ }
+
+ // If the children take up more space than the parent.. overflow!
+ if (widthDiff < 0) {
+ // Re-show the ellipsis now that we know it's needed.
+ aEllipsis.hidden = false;
+
+ // Separate text components with a ndash --
+ aParentBox.tooltipText = tooltip.join(" \u2013 ");
+ }
+ }
+
+ _doUnderflow(aName) {
+ // Hide the ellipsis right when we know we're underflowing instead of
+ // waiting for the timeout to trigger the _setUpOverflow calculations.
+ this[aName + "Box"].tooltipText = "";
+ this[aName + "OverflowEllipsis"].hidden = true;
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozGlodacompleteBaseRichlistitem, [
+ Ci.nsIDOMXULSelectControlItemElement,
+ ]);
+
+ /**
+ * The MozGlodaContactChunkRichlistitem widget displays an autocomplete item with
+ * contact chunk: e.g. image, name and description of the contact.
+ *
+ * @augments MozGlodacompleteBaseRichlistitem
+ */
+ class MozGlodaContactChunkRichlistitem extends MozGlodacompleteBaseRichlistitem {
+ static get inheritedAttributes() {
+ return {
+ "description.ac-comment": "selected",
+ "label.ac-comment": "selected",
+ "description.ac-url-text": "selected",
+ "label.ac-url-text": "selected",
+ };
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+ this.setAttribute("is", "gloda-contact-chunk-richlistitem");
+ this.appendChild(
+ MozXULElement.parseXULToFragment(`
+ <vbox>
+ <hbox>
+ <hbox class="ac-title"
+ flex="1"
+ onunderflow="_doUnderflow('_name');">
+ <description class="ac-normal-text ac-comment"></description>
+ </hbox>
+ <label class="ac-ellipsis-after ac-comment"
+ hidden="true"></label>
+ </hbox>
+ <hbox>
+ <hbox class="ac-url"
+ flex="1"
+ onunderflow="_doUnderflow('_identity');">
+ <description class="ac-normal-text ac-url-text"></description>
+ </hbox>
+ <label class="ac-ellipsis-after ac-url-text"
+ hidden="true"></label>
+ </hbox>
+ </vbox>
+ `)
+ );
+
+ let ellipsis = "\u2026";
+ try {
+ ellipsis = Services.prefs.getComplexValue(
+ "intl.ellipsis",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ } catch (ex) {
+ // Do nothing.. we already have a default.
+ }
+
+ this._identityOverflowEllipsis = this.querySelector("label.ac-url-text");
+ this._nameOverflowEllipsis = this.querySelector("label.ac-comment");
+
+ this._identityOverflowEllipsis.value = ellipsis;
+ this._nameOverflowEllipsis.value = ellipsis;
+
+ this._identityBox = this.querySelector(".ac-url");
+ this._identity = this.querySelector("description.ac-url-text");
+
+ this._nameBox = this.querySelector(".ac-title");
+ this._name = this.querySelector("description.ac-comment");
+
+ this._adjustAcItem();
+
+ this.initializeAttributeInheritance();
+ }
+
+ get label() {
+ let identity = this.obj;
+ return identity.accessibleLabel;
+ }
+
+ _adjustAcItem() {
+ let contact = this.obj;
+
+ if (contact == null) {
+ return;
+ }
+
+ let identity = contact.identities[0];
+
+ // Emphasize the matching search terms for the description.
+ this._setUpDescription(this._name, contact.name);
+ this._setUpDescription(this._identity, identity.value);
+
+ // Set up overflow on a timeout because the contents of the box
+ // might not have a width yet even though we just changed them.
+ setTimeout(
+ this._setUpOverflow,
+ 0,
+ this._nameBox,
+ this._nameOverflowEllipsis
+ );
+ setTimeout(
+ this._setUpOverflow,
+ 0,
+ this._identityBox,
+ this._identityOverflowEllipsis
+ );
+ }
+ }
+
+ customElements.define(
+ "gloda-contact-chunk-richlistitem",
+ MozGlodaContactChunkRichlistitem,
+ {
+ extends: "richlistitem",
+ }
+ );
+
+ /**
+ * The MozGlodaFulltextAllRichlistitem widget displays an autocomplete full text of
+ * all the items: e.g. full text explanation of the item.
+ *
+ * @augments MozGlodacompleteBaseRichlistitem
+ */
+ class MozGlodaFulltextAllRichlistitem extends MozGlodacompleteBaseRichlistitem {
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+ this.setAttribute("is", "gloda-fulltext-all-richlistitem");
+ this._explanation = document.createXULElement("description");
+ this._explanation.classList.add("explanation");
+ let label = gGlodaCompleteStrings.GetStringFromName(
+ "glodaComplete.messagesMentioningMany.label"
+ );
+ this._explanation.setAttribute(
+ "value",
+ label.replace("#1", this.row.words.join(", "))
+ );
+ this.appendChild(this._explanation);
+ }
+
+ get label() {
+ return "full text search: " + this.row.item; // what is this for? l10n?
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozGlodaFulltextAllRichlistitem, [
+ Ci.nsIDOMXULSelectControlItemElement,
+ ]);
+
+ customElements.define(
+ "gloda-fulltext-all-richlistitem",
+ MozGlodaFulltextAllRichlistitem,
+ {
+ extends: "richlistitem",
+ }
+ );
+
+ /**
+ * The MozGlodaFulltextAllRichlistitem widget displays an autocomplete full text
+ * of single item: e.g. full text explanation of the item.
+ *
+ * @augments MozGlodacompleteBaseRichlistitem
+ */
+ class MozGlodaFulltextSingleRichlistitem extends MozGlodacompleteBaseRichlistitem {
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+ this.setAttribute("is", "gloda-fulltext-single-richlistitem");
+ this._explanation = document.createXULElement("description");
+ this._explanation.classList.add("explanation", "gloda-fulltext-single");
+ this._parameters = document.createXULElement("description");
+
+ this.appendChild(this._explanation);
+ this.appendChild(this._parameters);
+
+ let label = gGlodaCompleteStrings.GetStringFromName(
+ "glodaComplete.messagesMentioning.label"
+ );
+ this._explanation.setAttribute(
+ "value",
+ label.replace("#1", this.row.item)
+ );
+ }
+
+ get label() {
+ return "full text search: " + this.row.item;
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozGlodaFulltextSingleRichlistitem, [
+ Ci.nsIDOMXULSelectControlItemElement,
+ ]);
+
+ customElements.define(
+ "gloda-fulltext-single-richlistitem",
+ MozGlodaFulltextSingleRichlistitem,
+ {
+ extends: "richlistitem",
+ }
+ );
+
+ /**
+ * The MozGlodaMultiRichlistitem widget displays an autocomplete description of multiple
+ * type items: e.g. explanation of the items.
+ *
+ * @augments MozGlodacompleteBaseRichlistitem
+ */
+ class MozGlodaMultiRichlistitem extends MozGlodacompleteBaseRichlistitem {
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+ this.setAttribute("is", "gloda-multi-richlistitem");
+ this._explanation = document.createXULElement("description");
+ this._identityHolder = document.createXULElement("hbox");
+ this._identityHolder.setAttribute("flex", "1");
+
+ this.appendChild(this._explanation);
+ this.appendChild(this._identityHolder);
+ this._adjustAcItem();
+ }
+
+ get label() {
+ return this._explanation.value;
+ }
+
+ renderItem(aObj) {
+ let node = document.createXULElement("richlistitem");
+
+ node.obj = aObj;
+ node.setAttribute(
+ "type",
+ "gloda-" + this.row.nounDef.name + "-chunk-richlistitem"
+ );
+
+ this._identityHolder.appendChild(node);
+ }
+
+ _adjustAcItem() {
+ // clear out any lingering children.
+ while (this._identityHolder.hasChildNodes()) {
+ this._identityHolder.lastChild.remove();
+ }
+
+ let row = this.row;
+ if (row == null) {
+ return;
+ }
+
+ this._explanation.value =
+ row.nounDef.name + "s " + row.criteriaType + "ed " + row.criteria;
+
+ // render anyone already in there.
+ for (let item of row.collection.items) {
+ this.renderItem(item);
+ }
+ // listen up, yo.
+ row.renderer = this;
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozGlodaMultiRichlistitem, [
+ Ci.nsIDOMXULSelectControlItemElement,
+ ]);
+
+ customElements.define("gloda-multi-richlistitem", MozGlodaMultiRichlistitem, {
+ extends: "richlistitem",
+ });
+
+ /**
+ * The MozGlodaSingleIdentityRichlistitem widget displays an autocomplete item with
+ * single identity: e.g. image, name and description of the item.
+ *
+ * @augments MozGlodacompleteBaseRichlistitem
+ */
+ class MozGlodaSingleIdentityRichlistitem extends MozGlodacompleteBaseRichlistitem {
+ static get inheritedAttributes() {
+ return {
+ "description.ac-comment": "selected",
+ "label.ac-comment": "selected",
+ "description.ac-url-text": "selected",
+ "label.ac-url-text": "selected",
+ };
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+
+ this.setAttribute("is", "gloda-single-identity-richlistitem");
+ this.appendChild(
+ MozXULElement.parseXULToFragment(`
+ <hbox class="gloda-single-identity">
+ <vbox>
+ <hbox>
+ <hbox class="ac-title"
+ flex="1"
+ onunderflow="_doUnderflow('_name');">
+ <description class="ac-normal-text ac-comment"></description>
+ </hbox>
+ <label class="ac-ellipsis-after ac-comment"
+ hidden="true"></label>
+ </hbox>
+ <hbox>
+ <hbox class="ac-url"
+ flex="1"
+ onunderflow="_doUnderflow('_identity');">
+ <description class="ac-normal-text ac-url-text"
+ inherits="selected"></description>
+ </hbox>
+ <label class="ac-ellipsis-after ac-url-text"
+ hidden="true"></label>
+ </hbox>
+ </vbox>
+ </hbox>
+ `)
+ );
+
+ let ellipsis = "\u2026";
+ try {
+ ellipsis = Services.prefs.getComplexValue(
+ "intl.ellipsis",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ } catch (ex) {
+ // Do nothing.. we already have a default.
+ }
+
+ this._identityOverflowEllipsis = this.querySelector("label.ac-url-text");
+ this._nameOverflowEllipsis = this.querySelector("label.ac-comment");
+
+ this._identityOverflowEllipsis.value = ellipsis;
+ this._nameOverflowEllipsis.value = ellipsis;
+
+ this._identityBox = this.querySelector(".ac-url");
+ this._identity = this.querySelector("description.ac-url-text");
+
+ this._nameBox = this.querySelector(".ac-title");
+ this._name = this.querySelector("description.ac-comment");
+
+ this._adjustAcItem();
+
+ this.initializeAttributeInheritance();
+ }
+
+ get label() {
+ let identity = this.row.item;
+ return identity.accessibleLabel;
+ }
+
+ _adjustAcItem() {
+ let identity = this.row.item;
+
+ if (identity == null) {
+ return;
+ }
+
+ // Emphasize the matching search terms for the description.
+ this._setUpDescription(this._name, identity.contact.name);
+ this._setUpDescription(this._identity, identity.value);
+
+ // Set up overflow on a timeout because the contents of the box
+ // might not have a width yet even though we just changed them.
+ setTimeout(
+ this._setUpOverflow,
+ 0,
+ this._nameBox,
+ this._nameOverflowEllipsis
+ );
+ setTimeout(
+ this._setUpOverflow,
+ 0,
+ this._identityBox,
+ this._identityOverflowEllipsis
+ );
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozGlodaSingleIdentityRichlistitem, [
+ Ci.nsIDOMXULSelectControlItemElement,
+ ]);
+
+ customElements.define(
+ "gloda-single-identity-richlistitem",
+ MozGlodaSingleIdentityRichlistitem,
+ {
+ extends: "richlistitem",
+ }
+ );
+
+ /**
+ * The MozGlodaSingleTagRichlistitem widget displays an autocomplete item with
+ * single tag: e.g. explanation of the item.
+ *
+ * @augments MozGlodacompleteBaseRichlistitem
+ */
+ class MozGlodaSingleTagRichlistitem extends MozGlodacompleteBaseRichlistitem {
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+ this.setAttribute("is", "gloda-single-tag-richlistitem");
+ this._explanation = document.createXULElement("description");
+ this._explanation.classList.add("explanation", "gloda-single");
+ this.appendChild(this._explanation);
+ let label = gGlodaCompleteStrings.GetStringFromName(
+ "glodaComplete.messagesTagged.label"
+ );
+ this._explanation.setAttribute(
+ "value",
+ label.replace("#1", this.row.item.tag)
+ );
+ }
+
+ get label() {
+ return "tag " + this.row.item.tag;
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozGlodaSingleTagRichlistitem, [
+ Ci.nsIDOMXULSelectControlItemElement,
+ ]);
+
+ customElements.define(
+ "gloda-single-tag-richlistitem",
+ MozGlodaSingleTagRichlistitem,
+ {
+ extends: "richlistitem",
+ }
+ );
+}
diff --git a/comm/mailnews/db/gloda/content/glodacomplete.js b/comm/mailnews/db/gloda/content/glodacomplete.js
new file mode 100644
index 0000000000..64578d4143
--- /dev/null
+++ b/comm/mailnews/db/gloda/content/glodacomplete.js
@@ -0,0 +1,466 @@
+/* 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/. */
+
+/* globals MozElements, MozXULElement */
+
+"use strict";
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ const MozPopupElement = MozElements.MozElementMixin(XULPopupElement);
+
+ /**
+ * The MozGlodacompleteRichResultPopup class creates the panel
+ * to append all the results for the gloda search autocomplete.
+ *
+ * @augments {MozPopupElement}
+ */
+ class MozGlodacompleteRichResultPopup extends MozPopupElement {
+ constructor() {
+ super();
+
+ this.addEventListener("popupshowing", event => {
+ // If normalMaxRows wasn't already set by the input, then set it here
+ // so that we restore the correct number when the popup is hidden.
+
+ // Null-check this.mInput; see bug 1017914
+ if (this._normalMaxRows < 0 && this.mInput) {
+ this._normalMaxRows = this.mInput.maxRows;
+ }
+
+ this.mPopupOpen = true;
+ });
+
+ this.addEventListener("popupshown", event => {
+ if (this._adjustHeightOnPopupShown) {
+ delete this._adjustHeightOnPopupShown;
+ this.adjustHeight();
+ }
+ });
+
+ this.addEventListener("popuphiding", event => {
+ let isListActive = true;
+ if (this.selectedIndex == -1) {
+ isListActive = false;
+ }
+ this.mInput.controller.stopSearch();
+ this.mPopupOpen = false;
+
+ // Reset the maxRows property to the cached "normal" value (if there's
+ // any), and reset normalMaxRows so that we can detect whether it was set
+ // by the input when the popupshowing handler runs.
+
+ // Null-check this.mInput; see bug 1017914
+ if (this.mInput && this._normalMaxRows > 0) {
+ this.mInput.maxRows = this._normalMaxRows;
+ }
+ this._normalMaxRows = -1;
+ // If the list was being navigated and then closed, make sure
+ // we fire accessible focus event back to textbox
+
+ // Null-check this.mInput; see bug 1017914
+ if (isListActive && this.mInput) {
+ this.mInput.mIgnoreFocus = true;
+ this.mInput._focus();
+ this.mInput.mIgnoreFocus = false;
+ }
+ });
+
+ this.attachShadow({ mode: "open" });
+
+ let slot = document.createElement("slot");
+ slot.part = "content";
+ this.shadowRoot.appendChild(slot);
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+ this.textContent = "";
+
+ this.mInput = null;
+
+ this.mPopupOpen = false;
+
+ this._currentIndex = 0;
+
+ /**
+ * This is the default number of rows that we give the autocomplete
+ * popup when the textbox doesn't have a "maxrows" attribute
+ * for us to use.
+ */
+ this.defaultMaxRows = 6;
+
+ /**
+ * In some cases (e.g. when the input's dropmarker button is clicked),
+ * the input wants to display a popup with more rows. In that case, it
+ * should increase its maxRows property and store the "normal" maxRows
+ * in this field. When the popup is hidden, we restore the input's
+ * maxRows to the value stored in this field.
+ *
+ * This field is set to -1 between uses so that we can tell when it's
+ * been set by the input and when we need to set it in the popupshowing
+ * handler.
+ */
+ this._normalMaxRows = -1;
+
+ this._previousSelectedIndex = -1;
+
+ this.mLastMoveTime = Date.now();
+
+ this.mousedOverIndex = -1;
+
+ this.richlistbox = document.createXULElement("richlistbox");
+ this.richlistbox.setAttribute("flex", "1");
+ this.richlistbox.classList.add("autocomplete-richlistbox");
+
+ this.appendChild(this.richlistbox);
+
+ if (!this.listEvents) {
+ this.listEvents = {
+ handleEvent: event => {
+ if (!this.parentNode) {
+ return;
+ }
+
+ switch (event.type) {
+ case "mouseup":
+ // Don't call onPopupClick for the scrollbar buttons, thumb,
+ // slider, etc. If we hit the richlistbox and not a
+ // richlistitem, we ignore the event.
+ if (
+ event.target.closest("richlistbox, richlistitem").localName ==
+ "richlistitem"
+ ) {
+ this.onPopupClick(event);
+ }
+ break;
+ case "mousemove":
+ if (Date.now() - this.mLastMoveTime <= 30) {
+ return;
+ }
+
+ let item = event.target.closest("richlistbox, richlistitem");
+
+ // If we hit the richlistbox and not a richlistitem, we ignore
+ // the event.
+ if (item.localName == "richlistbox") {
+ return;
+ }
+
+ let index = this.richlistbox.getIndexOfItem(item);
+
+ this.mousedOverIndex = index;
+
+ if (item.selectedByMouseOver) {
+ this.richlistbox.selectedIndex = index;
+ }
+
+ this.mLastMoveTime = Date.now();
+ break;
+ }
+ },
+ };
+ this.richlistbox.addEventListener("mouseup", this.listEvents);
+ this.richlistbox.addEventListener("mousemove", this.listEvents);
+ }
+ }
+
+ // nsIAutoCompletePopup
+ get input() {
+ return this.mInput;
+ }
+
+ get overrideValue() {
+ return null;
+ }
+
+ get popupOpen() {
+ return this.mPopupOpen;
+ }
+
+ get maxRows() {
+ return (this.mInput && this.mInput.maxRows) || this.defaultMaxRows;
+ }
+
+ set selectedIndex(val) {
+ if (val != this.richlistbox.selectedIndex) {
+ this._previousSelectedIndex = this.richlistbox.selectedIndex;
+ }
+ this.richlistbox.selectedIndex = val;
+ // Since ensureElementIsVisible may cause an expensive Layout flush,
+ // invoke it only if there may be a scrollbar, so if we could fetch
+ // more results than we can show at once.
+ // maxResults is the maximum number of fetched results, maxRows is the
+ // maximum number of rows we show at once, without a scrollbar.
+ if (this.mPopupOpen && this.maxResults > this.maxRows) {
+ // when clearing the selection (val == -1, so selectedItem will be
+ // null), we want to scroll back to the top. see bug #406194
+ this.richlistbox.ensureElementIsVisible(
+ this.richlistbox.selectedItem || this.richlistbox.firstElementChild
+ );
+ }
+ }
+
+ get selectedIndex() {
+ return this.richlistbox.selectedIndex;
+ }
+
+ get maxResults() {
+ // This is how many richlistitems will be kept around.
+ // Note, this getter may be overridden, or instances
+ // can have the nomaxresults attribute set to have no
+ // limit.
+ if (this.getAttribute("nomaxresults") == "true") {
+ return Infinity;
+ }
+
+ return 20;
+ }
+
+ get matchCount() {
+ return Math.min(this.mInput.controller.matchCount, this.maxResults);
+ }
+
+ get overflowPadding() {
+ return Number(this.getAttribute("overflowpadding"));
+ }
+
+ set view(val) {}
+
+ get view() {
+ return this.mInput.controller;
+ }
+
+ closePopup() {
+ if (this.mPopupOpen) {
+ this.hidePopup();
+ this.style.removeProperty("--panel-width");
+ }
+ }
+
+ getNextIndex(aReverse, aAmount, aIndex, aMaxRow) {
+ if (aMaxRow < 0) {
+ return -1;
+ }
+
+ let newIdx = aIndex + (aReverse ? -1 : 1) * aAmount;
+ if (
+ (aReverse && aIndex == -1) ||
+ (newIdx > aMaxRow && aIndex != aMaxRow)
+ ) {
+ newIdx = aMaxRow;
+ } else if ((!aReverse && aIndex == -1) || (newIdx < 0 && aIndex != 0)) {
+ newIdx = 0;
+ }
+
+ if (
+ (newIdx < 0 && aIndex == 0) ||
+ (newIdx > aMaxRow && aIndex == aMaxRow)
+ ) {
+ aIndex = -1;
+ } else {
+ aIndex = newIdx;
+ }
+
+ return aIndex;
+ }
+
+ onPopupClick(aEvent) {
+ this.input.controller.handleEnter(true, aEvent);
+ }
+
+ onSearchBegin() {
+ this.mousedOverIndex = -1;
+
+ if (typeof this._onSearchBegin == "function") {
+ this._onSearchBegin();
+ }
+ }
+
+ openAutocompletePopup(aInput, aElement) {
+ // until we have "baseBinding", (see bug #373652) this allows
+ // us to override openAutocompletePopup(), but still call
+ // the method on the base class
+ this._openAutocompletePopup(aInput, aElement);
+ }
+
+ _openAutocompletePopup(aInput, aElement) {
+ if (!this.mPopupOpen) {
+ // It's possible that the panel is hidden initially
+ // to avoid impacting startup / new window performance
+ aInput.popup.hidden = false;
+
+ this.mInput = aInput;
+ // clear any previous selection, see bugs 400671 and 488357
+ this.selectedIndex = -1;
+
+ let width = aElement.getBoundingClientRect().width;
+ this.style.setProperty(
+ "--panel-width",
+ (width > 100 ? width : 100) + "px"
+ );
+ // invalidate() depends on the width attribute
+ this._invalidate();
+
+ this.openPopup(aElement, "after_start", 0, 0, false, false);
+ }
+ }
+
+ invalidate(reason) {
+ // Don't bother doing work if we're not even showing
+ if (!this.mPopupOpen) {
+ return;
+ }
+
+ this._invalidate(reason);
+ }
+
+ _invalidate(reason) {
+ setTimeout(() => this.adjustHeight(), 0);
+
+ // remove all child nodes because we never want to reuse them.
+ while (this.richlistbox.hasChildNodes()) {
+ this.richlistbox.lastChild.remove();
+ }
+
+ this._currentIndex = 0;
+ this._appendCurrentResult();
+ }
+
+ _collapseUnusedItems() {
+ let existingItemsCount = this.richlistbox.children.length;
+ for (let i = this.matchCount; i < existingItemsCount; ++i) {
+ let item = this.richlistbox.children[i];
+
+ item.collapsed = true;
+ if (typeof item._onCollapse == "function") {
+ item._onCollapse();
+ }
+ }
+ }
+
+ adjustHeight() {
+ // Figure out how many rows to show
+ let rows = this.richlistbox.children;
+ let numRows = Math.min(this.matchCount, this.maxRows, rows.length);
+
+ // Default the height to 0 if we have no rows to show
+ let height = 0;
+ if (numRows) {
+ let firstRowRect = rows[0].getBoundingClientRect();
+ if (this._rlbPadding == undefined) {
+ let style = window.getComputedStyle(this.richlistbox);
+ let paddingTop = parseInt(style.paddingTop) || 0;
+ let paddingBottom = parseInt(style.paddingBottom) || 0;
+ this._rlbPadding = paddingTop + paddingBottom;
+ }
+
+ // The class `forceHandleUnderflow` is for the item might need to
+ // handle OverUnderflow or Overflow when the height of an item will
+ // be changed dynamically.
+ for (let i = 0; i < numRows; i++) {
+ if (rows[i].classList.contains("forceHandleUnderflow")) {
+ rows[i].handleOverUnderflow();
+ }
+ }
+
+ let lastRowRect = rows[numRows - 1].getBoundingClientRect();
+ // Calculate the height to have the first row to last row shown
+ height = lastRowRect.bottom - firstRowRect.top + this._rlbPadding;
+ }
+
+ let currentHeight = this.richlistbox.getBoundingClientRect().height;
+ if (height <= currentHeight) {
+ this._collapseUnusedItems();
+ }
+ this.richlistbox.style.removeProperty("height");
+ // We need to get the ceiling of the calculated value to ensure that the box fully contains
+ // all of its contents and doesn't cause a scrollbar since nsIBoxObject only expects a
+ // `long`. e.g. if `height` is 99.5 the richlistbox would render at height 99px with a
+ // scrollbar for the extra 0.5px.
+ this.richlistbox.height = Math.ceil(height);
+ }
+
+ _appendCurrentResult() {
+ let controller = this.mInput.controller;
+ let glodaCompleter = Cc[
+ "@mozilla.org/autocomplete/search;1?name=gloda"
+ ].getService(Ci.nsIAutoCompleteSearch).wrappedJSObject;
+
+ // Process maxRows per chunk to improve performance and user experience
+ for (let i = 0; i < this.maxRows; i++) {
+ if (this._currentIndex >= this.matchCount) {
+ return;
+ }
+
+ let item;
+
+ // trim the leading/trailing whitespace
+ let trimmedSearchString = controller.searchString.trim();
+ let result = glodaCompleter.curResult;
+
+ item = document.createXULElement("richlistitem", {
+ is: result.getStyleAt(this._currentIndex),
+ });
+
+ // set these attributes before we set the class
+ // so that we can use them from the constructor
+ let row = result.getObjectAt(this._currentIndex);
+ item.setAttribute("text", trimmedSearchString);
+ item.setAttribute("type", result.getStyleAt(this._currentIndex));
+
+ item.row = row;
+
+ // set the class at the end so we can use the attributes
+ // in the xbl constructor
+ item.className = "autocomplete-richlistitem";
+ this.richlistbox.appendChild(item);
+ this._currentIndex++;
+ }
+
+ // yield after each batch of items so that typing the url bar is responsive
+ setTimeout(() => this._appendCurrentResult(), 0);
+ }
+
+ selectBy(aReverse, aPage) {
+ try {
+ let amount = aPage ? 5 : 1;
+
+ // because we collapsed unused items, we can't use this.richlistbox.getRowCount(), we need to use the matchCount
+ this.selectedIndex = this.getNextIndex(
+ aReverse,
+ amount,
+ this.selectedIndex,
+ this.matchCount - 1
+ );
+ if (this.selectedIndex == -1) {
+ this.input._focus();
+ }
+ } catch (ex) {
+ // do nothing - occasionally timer-related js errors happen here
+ // e.g. "this.selectedIndex has no properties", when you type fast and hit a
+ // navigation key before this popup has opened
+ }
+ }
+
+ disconnectedCallback() {
+ if (this.listEvents) {
+ this.richlistbox.removeEventListener("mouseup", this.listEvents);
+ this.richlistbox.removeEventListener("mousemove", this.listEvents);
+ delete this.listEvents;
+ }
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozGlodacompleteRichResultPopup, [
+ Ci.nsIAutoCompletePopup,
+ ]);
+ customElements.define(
+ "glodacomplete-rich-result-popup",
+ MozGlodacompleteRichResultPopup,
+ { extends: "panel" }
+ );
+}