diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/db/gloda/content | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mailnews/db/gloda/content')
-rw-r--r-- | comm/mailnews/db/gloda/content/autocomplete-richlistitem.js | 644 | ||||
-rw-r--r-- | comm/mailnews/db/gloda/content/glodacomplete.js | 466 |
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" } + ); +} |