/* 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(`
`)
);
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(`
`)
);
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",
}
);
}