mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-11 21:58:41 +02:00
1027 lines
34 KiB
JavaScript
1027 lines
34 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 is loaded into all XUL windows. Wrap in a block to prevent
|
|
// leaking to window scope.
|
|
{
|
|
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
|
|
MozElements.MozAutocompleteRichlistitem = class MozAutocompleteRichlistitem extends MozElements.MozRichlistitem {
|
|
constructor() {
|
|
super();
|
|
|
|
/**
|
|
* This overrides listitem's mousedown handler because we want to set the
|
|
* selected item even when the shift or accel keys are pressed.
|
|
*/
|
|
this.addEventListener("mousedown", (event) => {
|
|
// Call this.control only once since it's not a simple getter.
|
|
let control = this.control;
|
|
if (!control || control.disabled) {
|
|
return;
|
|
}
|
|
if (!this.selected) {
|
|
control.selectItem(this);
|
|
}
|
|
control.currentItem = this;
|
|
});
|
|
|
|
this.addEventListener("mouseover", (event) => {
|
|
// The point of implementing this handler is to allow drags to change
|
|
// the selected item. If the user mouses down on an item, it becomes
|
|
// selected. If they then drag the mouse to another item, select it.
|
|
// Handle all three primary mouse buttons: right, left, and wheel, since
|
|
// all three change the selection on mousedown.
|
|
let mouseDown = event.buttons & 0b111;
|
|
if (!mouseDown) {
|
|
return;
|
|
}
|
|
// Call this.control only once since it's not a simple getter.
|
|
let control = this.control;
|
|
if (!control || control.disabled) {
|
|
return;
|
|
}
|
|
if (!this.selected) {
|
|
control.selectItem(this);
|
|
}
|
|
control.currentItem = this;
|
|
});
|
|
|
|
this.addEventListener("overflow", () => this._onOverflow());
|
|
this.addEventListener("underflow", () => this._onUnderflow());
|
|
}
|
|
|
|
connectedCallback() {
|
|
if (this.delayConnectedCallback()) {
|
|
return;
|
|
}
|
|
|
|
this.textContent = "";
|
|
this.appendChild(MozXULElement.parseXULToFragment(this._markup));
|
|
|
|
this._boundaryCutoff = null;
|
|
this._inOverflow = false;
|
|
|
|
this._updateAttributes();
|
|
this._adjustAcItem();
|
|
}
|
|
|
|
static get observedAttributes() {
|
|
return [
|
|
"actiontype",
|
|
"current",
|
|
"selected",
|
|
"image",
|
|
"type",
|
|
];
|
|
}
|
|
|
|
get inheritedAttributeMap() {
|
|
if (!this.__inheritedAttributeMap) {
|
|
this.__inheritedAttributeMap = new Map([
|
|
[ this.querySelector(".ac-type-icon"), [ "selected", "current", "type" ] ],
|
|
[ this.querySelector(".ac-site-icon"), [ "src=image", "selected", "type" ] ],
|
|
[ this.querySelector(".ac-title"), [ "selected" ] ],
|
|
[ this.querySelector(".ac-title-text"), [ "selected" ] ],
|
|
[ this.querySelector(".ac-tags"), [ "selected" ] ],
|
|
[ this.querySelector(".ac-tags-text"), [ "selected" ] ],
|
|
[ this.querySelector(".ac-separator"), [ "selected", "actiontype", "type" ] ],
|
|
[ this.querySelector(".ac-url"), [ "selected", "actiontype" ] ],
|
|
[ this.querySelector(".ac-url-text"), [ "selected" ] ],
|
|
[ this.querySelector(".ac-action"), [ "selected", "actiontype" ] ],
|
|
[ this.querySelector(".ac-action-text"), [ "selected" ] ],
|
|
]);
|
|
}
|
|
return this.__inheritedAttributeMap;
|
|
}
|
|
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
if (this.isConnectedAndReady && oldValue != newValue &&
|
|
this.constructor.observedAttributes.includes(name)) {
|
|
this._updateAttributes();
|
|
}
|
|
}
|
|
|
|
_updateAttributes() {
|
|
for (let [ el, attrs ] of this.inheritedAttributeMap.entries()) {
|
|
for (let attr of attrs) {
|
|
this.inheritAttribute(el, attr);
|
|
}
|
|
}
|
|
}
|
|
|
|
get _markup() {
|
|
return `
|
|
<image class="ac-type-icon"></image>
|
|
<image class="ac-site-icon"></image>
|
|
<hbox class="ac-title" align="center">
|
|
<description class="ac-text-overflow-container">
|
|
<description class="ac-title-text"></description>
|
|
</description>
|
|
</hbox>
|
|
<hbox class="ac-tags" align="center">
|
|
<description class="ac-text-overflow-container">
|
|
<description class="ac-tags-text"></description>
|
|
</description>
|
|
</hbox>
|
|
<hbox class="ac-separator" align="center">
|
|
<description class="ac-separator-text" value="—"></description>
|
|
</hbox>
|
|
<hbox class="ac-url" align="center">
|
|
<description class="ac-text-overflow-container">
|
|
<description class="ac-url-text"></description>
|
|
</description>
|
|
</hbox>
|
|
<hbox class="ac-action" align="center">
|
|
<description class="ac-text-overflow-container">
|
|
<description class="ac-action-text"></description>
|
|
</description>
|
|
</hbox>
|
|
`;
|
|
}
|
|
|
|
get _typeIcon() {
|
|
return this.querySelector(".ac-type-icon");
|
|
}
|
|
|
|
get _titleText() {
|
|
return this.querySelector(".ac-title-text");
|
|
}
|
|
|
|
get _tags() {
|
|
return this.querySelector(".ac-tags");
|
|
}
|
|
|
|
get _tagsText() {
|
|
return this.querySelector(".ac-tags-text");
|
|
}
|
|
|
|
get _separator() {
|
|
return this.querySelector(".ac-separator");
|
|
}
|
|
|
|
get _urlText() {
|
|
return this.querySelector(".ac-url-text");
|
|
}
|
|
|
|
get _actionText() {
|
|
return this.querySelector(".ac-action-text");
|
|
}
|
|
|
|
get label() {
|
|
// This property is a string that is read aloud by screen readers,
|
|
// so it must not contain anything that should not be user-facing.
|
|
|
|
let parts = [
|
|
this.getAttribute("title"),
|
|
this.getAttribute("displayurl"),
|
|
];
|
|
let label = parts.filter(str => str).join(" ");
|
|
|
|
// allow consumers that have extended popups to override
|
|
// the label values for the richlistitems
|
|
let panel = this.parentNode.parentNode;
|
|
if (panel.createResultLabel) {
|
|
return panel.createResultLabel(this, label);
|
|
}
|
|
|
|
return label;
|
|
}
|
|
|
|
get _stringBundle() {
|
|
if (!this.__stringBundle) {
|
|
this.__stringBundle = Services.strings.createBundle(
|
|
"chrome://global/locale/autocomplete.properties"
|
|
);
|
|
}
|
|
return this.__stringBundle;
|
|
}
|
|
|
|
get boundaryCutoff() {
|
|
if (!this._boundaryCutoff) {
|
|
this._boundaryCutoff = Services.prefs.
|
|
getIntPref("toolkit.autocomplete.richBoundaryCutoff");
|
|
}
|
|
return this._boundaryCutoff;
|
|
}
|
|
|
|
_cleanup() {
|
|
this.removeAttribute("url");
|
|
this.removeAttribute("image");
|
|
this.removeAttribute("title");
|
|
this.removeAttribute("text");
|
|
this.removeAttribute("displayurl");
|
|
}
|
|
|
|
_onOverflow() {
|
|
this._inOverflow = true;
|
|
this._handleOverflow();
|
|
}
|
|
|
|
_onUnderflow() {
|
|
this._inOverflow = false;
|
|
this._handleOverflow();
|
|
}
|
|
|
|
_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 Array.prototype.slice.call(aSearchTokens)) {
|
|
let matchIndex = -1;
|
|
let searchLen = search.length;
|
|
|
|
// Find all matches of the search terms, but stop early for perf
|
|
let lowerText = aText.substr(0, this.boundaryCutoff).toLowerCase();
|
|
while ((matchIndex = lowerText.indexOf(search, matchIndex + 1)) >= 0) {
|
|
regions.push([matchIndex, matchIndex + searchLen]);
|
|
}
|
|
}
|
|
|
|
// Sort the regions by start position then end position
|
|
regions = regions.sort((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 = [];
|
|
let len = regions.length;
|
|
for (let i = 0; i < len; 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+/);
|
|
}
|
|
|
|
_setUpDescription(aDescriptionElement, aText, aNoEmphasis) {
|
|
// Get rid of all previous text
|
|
if (!aDescriptionElement) {
|
|
return;
|
|
}
|
|
while (aDescriptionElement.hasChildNodes())
|
|
aDescriptionElement.firstChild.remove();
|
|
|
|
// If aNoEmphasis is specified, don't add any emphasis
|
|
if (aNoEmphasis) {
|
|
aDescriptionElement.appendChild(document.createTextNode(aText));
|
|
return;
|
|
}
|
|
|
|
// 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);
|
|
|
|
this._appendDescriptionSpans(indices, aText, aDescriptionElement,
|
|
aDescriptionElement);
|
|
}
|
|
|
|
_appendDescriptionSpans(indices, text, spansParentElement, descriptionElement) {
|
|
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 spanText = text.substr(start, next - start);
|
|
start = next;
|
|
|
|
if (i % 2 == 0) {
|
|
// Emphasize the text for even indices
|
|
let span = spansParentElement.appendChild(
|
|
document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
|
|
this._setUpEmphasisSpan(span, descriptionElement);
|
|
span.textContent = spanText;
|
|
} else {
|
|
// Otherwise, it's plain text
|
|
spansParentElement.appendChild(document.createTextNode(spanText));
|
|
}
|
|
}
|
|
}
|
|
|
|
_setUpTags(tags) {
|
|
while (this._tagsText.hasChildNodes()) {
|
|
this._tagsText.firstChild.remove();
|
|
}
|
|
|
|
let anyTagsMatch = false;
|
|
|
|
// Include only tags that match the search string.
|
|
for (let tag of tags) {
|
|
// Check if the tag matches the search string.
|
|
let search = this.getAttribute("text");
|
|
let tokens = this._getSearchTokens(search);
|
|
let indices = this._getBoundaryIndices(tag, tokens);
|
|
|
|
if (indices.length == 2 &&
|
|
indices[0] == 0 &&
|
|
indices[1] == tag.length) {
|
|
// The tag doesn't match the search string, so don't include it.
|
|
continue;
|
|
}
|
|
|
|
anyTagsMatch = true;
|
|
|
|
let tagSpan =
|
|
document.createElementNS("http://www.w3.org/1999/xhtml", "span");
|
|
tagSpan.classList.add("ac-tag");
|
|
this._tagsText.appendChild(tagSpan);
|
|
|
|
this._appendDescriptionSpans(indices, tag, tagSpan, this._tagsText);
|
|
}
|
|
|
|
return anyTagsMatch;
|
|
}
|
|
|
|
_setUpEmphasisSpan(aSpan, aDescriptionElement) {
|
|
aSpan.classList.add("ac-emphasize-text");
|
|
switch (aDescriptionElement) {
|
|
case this._titleText:
|
|
aSpan.classList.add("ac-emphasize-text-title");
|
|
break;
|
|
case this._tagsText:
|
|
aSpan.classList.add("ac-emphasize-text-tag");
|
|
break;
|
|
case this._urlText:
|
|
aSpan.classList.add("ac-emphasize-text-url");
|
|
break;
|
|
case this._actionText:
|
|
aSpan.classList.add("ac-emphasize-text-action");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This will generate an array of emphasis pairs for use with
|
|
* _setUpEmphasisedSections(). Each pair is a tuple (array) that
|
|
* represents a block of text - containing the text of that block, and a
|
|
* boolean for whether that block should have an emphasis styling applied
|
|
* to it.
|
|
*
|
|
* These pairs are generated by parsing a localised string (aSourceString)
|
|
* with parameters, in the format that is used by
|
|
* nsIStringBundle.formatStringFromName():
|
|
*
|
|
* "textA %1$S textB textC %2$S"
|
|
*
|
|
* Or:
|
|
*
|
|
* "textA %S"
|
|
*
|
|
* Where "%1$S", "%2$S", and "%S" are intended to be replaced by provided
|
|
* replacement strings. These are specified an array of tuples
|
|
* (aReplacements), each containing the replacement text and a boolean for
|
|
* whether that text should have an emphasis styling applied. This is used
|
|
* as a 1-based array - ie, "%1$S" is replaced by the item in the first
|
|
* index of aReplacements, "%2$S" by the second, etc. "%S" will always
|
|
* match the first index.
|
|
*/
|
|
_generateEmphasisPairs(aSourceString, aReplacements) {
|
|
let pairs = [];
|
|
|
|
// Split on %S, %1$S, %2$S, etc. ie:
|
|
// "textA %S"
|
|
// becomes ["textA ", "%S"]
|
|
// "textA %1$S textB textC %2$S"
|
|
// becomes ["textA ", "%1$S", " textB textC ", "%2$S"]
|
|
let parts = aSourceString.split(/(%(?:[0-9]+\$)?S)/);
|
|
|
|
for (let part of parts) {
|
|
// The above regex will actually give us an empty string at the
|
|
// end - we don't want that, as we don't want to later generate an
|
|
// empty text node for it.
|
|
if (part.length === 0)
|
|
continue;
|
|
|
|
// Determine if this token is a replacement token or a normal text
|
|
// token. If it is a replacement token, we want to extract the
|
|
// numerical number. However, we still want to match on "$S".
|
|
let match = part.match(/^%(?:([0-9]+)\$)?S$/);
|
|
|
|
if (match) {
|
|
// "%S" doesn't have a numerical number in it, but will always
|
|
// be assumed to be 1. Furthermore, the input string specifies
|
|
// these with a 1-based index, but we want a 0-based index.
|
|
let index = (match[1] || 1) - 1;
|
|
|
|
if (index >= 0 && index < aReplacements.length) {
|
|
pairs.push([...aReplacements[index]]);
|
|
}
|
|
} else {
|
|
pairs.push([part]);
|
|
}
|
|
}
|
|
|
|
return pairs;
|
|
}
|
|
|
|
/**
|
|
* _setUpEmphasisedSections() has the same use as _setUpDescription,
|
|
* except instead of taking a string and highlighting given tokens, it takes
|
|
* an array of pairs generated by _generateEmphasisPairs(). This allows
|
|
* control over emphasising based on specific blocks of text, rather than
|
|
* search for substrings.
|
|
*/
|
|
_setUpEmphasisedSections(aDescriptionElement, aTextPairs) {
|
|
// Get rid of all previous text
|
|
while (aDescriptionElement.hasChildNodes())
|
|
aDescriptionElement.firstChild.remove();
|
|
|
|
for (let [text, emphasise] of aTextPairs) {
|
|
if (emphasise) {
|
|
let span = aDescriptionElement.appendChild(
|
|
document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
|
|
span.textContent = text;
|
|
switch (emphasise) {
|
|
case "match":
|
|
this._setUpEmphasisSpan(span, aDescriptionElement);
|
|
break;
|
|
}
|
|
} else {
|
|
aDescriptionElement.appendChild(document.createTextNode(text));
|
|
}
|
|
}
|
|
}
|
|
|
|
_unescapeUrl(url) {
|
|
return Services.textToSubURI.unEscapeURIForUI("UTF-8", url);
|
|
}
|
|
|
|
_reuseAcItem() {
|
|
let action = this._parseActionUrl(this.getAttribute("url"));
|
|
let popup = this.parentNode.parentNode;
|
|
|
|
// If the item is a searchengine action, then it should
|
|
// only be reused if the engine name is the same as the
|
|
// popup's override engine name, if any.
|
|
if (!action ||
|
|
action.type != "searchengine" ||
|
|
!popup.overrideSearchEngineName ||
|
|
action.params.engineName == popup.overrideSearchEngineName) {
|
|
this.collapsed = false;
|
|
|
|
// The popup may have changed size between now and the last
|
|
// time the item was shown, so always handle over/underflow.
|
|
let dwu = window.windowUtils;
|
|
let popupWidth = dwu.getBoundsWithoutFlushing(this.parentNode).width;
|
|
if (!this._previousPopupWidth || this._previousPopupWidth != popupWidth) {
|
|
this._previousPopupWidth = popupWidth;
|
|
this.handleOverUnderflow();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
_adjustAcItem() {
|
|
let originalUrl = this.getAttribute("ac-value");
|
|
let title = this.getAttribute("ac-comment");
|
|
this.setAttribute("url", originalUrl);
|
|
this.setAttribute("image", this.getAttribute("ac-image"));
|
|
this.setAttribute("title", title);
|
|
this.setAttribute("text", this.getAttribute("ac-text"));
|
|
|
|
let popup = this.parentNode.parentNode;
|
|
let titleLooksLikeUrl = false;
|
|
let displayUrl = originalUrl;
|
|
let emphasiseUrl = true;
|
|
let trimDisplayUrl = true;
|
|
|
|
let type = this.getAttribute("originaltype");
|
|
let types = new Set(type.split(/\s+/));
|
|
let initialTypes = new Set(types);
|
|
// Remove types that should ultimately not be in the `type` string.
|
|
types.delete("action");
|
|
types.delete("autofill");
|
|
types.delete("heuristic");
|
|
type = [...types][0] || "";
|
|
|
|
let action;
|
|
|
|
if (initialTypes.has("autofill") && !initialTypes.has("action")) {
|
|
// Treat autofills as visiturl actions, unless they are already also
|
|
// actions.
|
|
action = {
|
|
type: "visiturl",
|
|
params: { url: title },
|
|
};
|
|
}
|
|
|
|
this.removeAttribute("actiontype");
|
|
this.classList.remove(
|
|
"overridable-action",
|
|
"emptySearchQuery",
|
|
"aliasOffer"
|
|
);
|
|
|
|
// If the type includes an action, set up the item appropriately.
|
|
if (initialTypes.has("action") || action) {
|
|
action = action || this._parseActionUrl(originalUrl);
|
|
this.setAttribute("actiontype", action.type);
|
|
|
|
switch (action.type) {
|
|
case "switchtab":
|
|
{
|
|
this.classList.add("overridable-action");
|
|
displayUrl = action.params.url;
|
|
let desc = this._stringBundle.GetStringFromName("switchToTab2");
|
|
this._setUpDescription(this._actionText, desc, true);
|
|
break;
|
|
}
|
|
case "remotetab":
|
|
{
|
|
displayUrl = action.params.url;
|
|
let desc = action.params.deviceName;
|
|
this._setUpDescription(this._actionText, desc, true);
|
|
break;
|
|
}
|
|
case "searchengine":
|
|
{
|
|
emphasiseUrl = false;
|
|
|
|
// The order here is not localizable, we default to appending
|
|
// "- Search with Engine" to the search string, to be able to
|
|
// properly generate emphasis pairs. That said, no localization
|
|
// changed the order while it was possible, so doesn't look like
|
|
// there's a strong need for that.
|
|
let {
|
|
engineName,
|
|
searchSuggestion,
|
|
searchQuery,
|
|
alias,
|
|
} = action.params;
|
|
|
|
// Override the engine name if the popup defines an override.
|
|
let override = popup.overrideSearchEngineName;
|
|
if (override && override != engineName) {
|
|
engineName = override;
|
|
action.params.engineName = override;
|
|
let newURL =
|
|
PlacesUtils.mozActionURI(action.type, action.params);
|
|
this.setAttribute("url", newURL);
|
|
}
|
|
|
|
let engineStr =
|
|
this._stringBundle.formatStringFromName("searchWithEngine", [engineName], 1);
|
|
this._setUpDescription(this._actionText, engineStr, true);
|
|
|
|
// Make the title by generating an array of pairs and its
|
|
// corresponding interpolation string (e.g., "%1$S") to pass to
|
|
// _generateEmphasisPairs.
|
|
let pairs;
|
|
if (searchSuggestion) {
|
|
// Check if the search query appears in the suggestion. It may
|
|
// not. If it does, then emphasize the query in the suggestion
|
|
// and otherwise just include the suggestion without emphasis.
|
|
let idx = searchSuggestion.indexOf(searchQuery);
|
|
if (idx >= 0) {
|
|
pairs = [
|
|
[searchSuggestion.substring(0, idx), ""],
|
|
[searchQuery, "match"],
|
|
[searchSuggestion.substring(idx + searchQuery.length), ""],
|
|
];
|
|
} else {
|
|
pairs = [
|
|
[searchSuggestion, ""],
|
|
];
|
|
}
|
|
} else if (alias &&
|
|
!searchQuery.trim() &&
|
|
!initialTypes.has("heuristic")) {
|
|
// For non-heuristic alias results that have an empty query, we
|
|
// want to show "@engine -- Search with Engine" to make it clear
|
|
// that the user can search by selecting the result and using
|
|
// the alias. Normally we hide the "Search with Engine" part
|
|
// until the result is selected or moused over, but not here.
|
|
// Add the aliasOffer class so we can detect this in the CSS.
|
|
this.classList.add("aliasOffer");
|
|
pairs = [
|
|
[alias, ""],
|
|
];
|
|
} else {
|
|
// Add the emptySearchQuery class if the search query is the
|
|
// empty string. We use it to hide .ac-separator in CSS.
|
|
if (!searchQuery.trim()) {
|
|
this.classList.add("emptySearchQuery");
|
|
}
|
|
pairs = [
|
|
[searchQuery, ""],
|
|
];
|
|
}
|
|
let interpStr = pairs.map((pair, i) => `%${i + 1}$S`).join("");
|
|
title = this._generateEmphasisPairs(interpStr, pairs);
|
|
|
|
// If this is a default search match, we remove the image so we
|
|
// can style it ourselves with a generic search icon.
|
|
// We don't do this when matching an aliased search engine,
|
|
// because the icon helps with recognising which engine will be
|
|
// used (when using the default engine, we don't need that
|
|
// recognition).
|
|
if (!action.params.alias && !initialTypes.has("favicon")) {
|
|
this.removeAttribute("image");
|
|
}
|
|
break;
|
|
}
|
|
case "visiturl":
|
|
{
|
|
emphasiseUrl = false;
|
|
displayUrl = action.params.url;
|
|
titleLooksLikeUrl = true;
|
|
let visitStr = this._stringBundle.GetStringFromName("visit");
|
|
this._setUpDescription(this._actionText, visitStr, true);
|
|
break;
|
|
}
|
|
case "extension":
|
|
{
|
|
let content = action.params.content;
|
|
displayUrl = content;
|
|
trimDisplayUrl = false;
|
|
this._setUpDescription(this._actionText, content, true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (trimDisplayUrl) {
|
|
let input = popup.input;
|
|
if (typeof input.trimValue == "function")
|
|
displayUrl = input.trimValue(displayUrl);
|
|
displayUrl = this._unescapeUrl(displayUrl);
|
|
}
|
|
// For performance reasons we may want to limit the displayUrl size.
|
|
if (popup.textRunsMaxLen && displayUrl) {
|
|
displayUrl = displayUrl.substr(0, popup.textRunsMaxLen);
|
|
}
|
|
this.setAttribute("displayurl", displayUrl);
|
|
|
|
// Show the domain as the title if we don't have a title.
|
|
if (!title) {
|
|
titleLooksLikeUrl = true;
|
|
try {
|
|
let uri = Services.io.newURI(originalUrl);
|
|
// Not all valid URLs have a domain.
|
|
if (uri.host)
|
|
title = uri.host;
|
|
} catch (e) {}
|
|
if (!title)
|
|
title = displayUrl;
|
|
}
|
|
|
|
this._tags.setAttribute("empty", "true");
|
|
|
|
if (type == "tag" || type == "bookmark-tag") {
|
|
// The title is separated from the tags by an endash
|
|
let tags;
|
|
[, title, tags] = title.match(/^(.+) \u2013 (.+)$/);
|
|
|
|
// Each tag is split by a comma in an undefined order, so sort it
|
|
let sortedTags = tags.split(/\s*,\s*/).sort((a, b) => {
|
|
return a.localeCompare(a);
|
|
});
|
|
|
|
let anyTagsMatch = this._setUpTags(sortedTags);
|
|
if (anyTagsMatch) {
|
|
this._tags.removeAttribute("empty");
|
|
}
|
|
if (type == "bookmark-tag") {
|
|
type = "bookmark";
|
|
}
|
|
} else if (type == "keyword") {
|
|
// Note that this is a moz-action with action.type == keyword.
|
|
emphasiseUrl = false;
|
|
let keywordArg = this.getAttribute("text").replace(/^[^\s]+\s*/, "");
|
|
if (!keywordArg) {
|
|
// Treat keyword searches without arguments as visiturl actions.
|
|
type = "visiturl";
|
|
this.setAttribute("actiontype", "visiturl");
|
|
let visitStr = this._stringBundle.GetStringFromName("visit");
|
|
this._setUpDescription(this._actionText, visitStr, true);
|
|
} else {
|
|
let pairs = [
|
|
[title, ""],
|
|
[keywordArg, "match"],
|
|
];
|
|
let interpStr =
|
|
this._stringBundle.GetStringFromName("bookmarkKeywordSearch");
|
|
title = this._generateEmphasisPairs(interpStr, pairs);
|
|
// The action box will be visible since this is a moz-action, but
|
|
// we want it to appear as if it were not visible, so set its text
|
|
// to the empty string.
|
|
this._setUpDescription(this._actionText, "", false);
|
|
}
|
|
}
|
|
|
|
this.setAttribute("type", type);
|
|
|
|
if (titleLooksLikeUrl) {
|
|
this._titleText.setAttribute("lookslikeurl", "true");
|
|
} else {
|
|
this._titleText.removeAttribute("lookslikeurl");
|
|
}
|
|
|
|
if (Array.isArray(title)) {
|
|
// For performance reasons we may want to limit the title size.
|
|
if (popup.textRunsMaxLen) {
|
|
title.forEach(t => {
|
|
// Limit all the even items.
|
|
for (let i = 0; i < t.length; i += 2) {
|
|
t[i] = t[i].substr(0, popup.textRunsMaxLen);
|
|
}
|
|
});
|
|
}
|
|
this._setUpEmphasisedSections(this._titleText, title);
|
|
} else {
|
|
// For performance reasons we may want to limit the title size.
|
|
if (popup.textRunsMaxLen && title) {
|
|
title = title.substr(0, popup.textRunsMaxLen);
|
|
}
|
|
this._setUpDescription(this._titleText, title, false);
|
|
}
|
|
this._setUpDescription(this._urlText, displayUrl, !emphasiseUrl);
|
|
|
|
// Removing the max-width may be jarring when the item is visible, but
|
|
// we have no other choice to properly crop the text.
|
|
// Removing max-widths may cause overflow or underflow events, that
|
|
// will set the _inOverflow property. In case both the old and the new
|
|
// text are overflowing, the overflow event won't happen, and we must
|
|
// enforce an _handleOverflow() call to update the max-widths.
|
|
let wasInOverflow = this._inOverflow;
|
|
this._removeMaxWidths();
|
|
if (wasInOverflow && this._inOverflow) {
|
|
this._handleOverflow();
|
|
}
|
|
}
|
|
|
|
_removeMaxWidths() {
|
|
if (this._hasMaxWidths) {
|
|
this._titleText.style.removeProperty("max-width");
|
|
this._tagsText.style.removeProperty("max-width");
|
|
this._urlText.style.removeProperty("max-width");
|
|
this._actionText.style.removeProperty("max-width");
|
|
this._hasMaxWidths = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method truncates the displayed strings as necessary.
|
|
*/
|
|
_handleOverflow() {
|
|
let itemRect = this.parentNode.getBoundingClientRect();
|
|
let titleRect = this._titleText.getBoundingClientRect();
|
|
let tagsRect = this._tagsText.getBoundingClientRect();
|
|
let separatorRect = this._separator.getBoundingClientRect();
|
|
let urlRect = this._urlText.getBoundingClientRect();
|
|
let actionRect = this._actionText.getBoundingClientRect();
|
|
let separatorURLActionWidth =
|
|
separatorRect.width + Math.max(urlRect.width, actionRect.width);
|
|
|
|
// Total width for the title and URL/action is the width of the item
|
|
// minus the start of the title text minus a little optional extra padding.
|
|
// This extra padding amount is basically arbitrary but keeps the text
|
|
// from getting too close to the popup's edge.
|
|
let dir = this.getAttribute("dir");
|
|
let titleStart = dir == "rtl" ? itemRect.right - titleRect.right :
|
|
titleRect.left - itemRect.left;
|
|
|
|
let popup = this.parentNode.parentNode;
|
|
let itemWidth = itemRect.width - titleStart - popup.overflowPadding -
|
|
(popup.margins ? popup.margins.end : 0);
|
|
|
|
if (this._tags.hasAttribute("empty")) {
|
|
// The tags box is not displayed in this case.
|
|
tagsRect.width = 0;
|
|
}
|
|
|
|
let titleTagsWidth = titleRect.width + tagsRect.width;
|
|
if (titleTagsWidth + separatorURLActionWidth > itemWidth) {
|
|
// Title + tags + URL/action overflows the item width.
|
|
|
|
// The percentage of the item width allocated to the title and tags.
|
|
let titleTagsPct = 0.66;
|
|
|
|
let titleTagsAvailable = itemWidth - separatorURLActionWidth;
|
|
let titleTagsMaxWidth = Math.max(
|
|
titleTagsAvailable,
|
|
itemWidth * titleTagsPct
|
|
);
|
|
if (titleTagsWidth > titleTagsMaxWidth) {
|
|
// Title + tags overflows the max title + tags width.
|
|
|
|
// The percentage of the title + tags width allocated to the
|
|
// title.
|
|
let titlePct = 0.33;
|
|
|
|
let titleAvailable = titleTagsMaxWidth - tagsRect.width;
|
|
let titleMaxWidth = Math.max(
|
|
titleAvailable,
|
|
titleTagsMaxWidth * titlePct
|
|
);
|
|
let tagsAvailable = titleTagsMaxWidth - titleRect.width;
|
|
let tagsMaxWidth = Math.max(
|
|
tagsAvailable,
|
|
titleTagsMaxWidth * (1 - titlePct)
|
|
);
|
|
this._titleText.style.maxWidth = titleMaxWidth + "px";
|
|
this._tagsText.style.maxWidth = tagsMaxWidth + "px";
|
|
}
|
|
let urlActionMaxWidth = Math.max(
|
|
itemWidth - titleTagsWidth,
|
|
itemWidth * (1 - titleTagsPct)
|
|
);
|
|
urlActionMaxWidth -= separatorRect.width;
|
|
this._urlText.style.maxWidth = urlActionMaxWidth + "px";
|
|
this._actionText.style.maxWidth = urlActionMaxWidth + "px";
|
|
this._hasMaxWidths = true;
|
|
}
|
|
}
|
|
|
|
handleOverUnderflow() {
|
|
this._removeMaxWidths();
|
|
this._handleOverflow();
|
|
}
|
|
|
|
_parseActionUrl(aUrl) {
|
|
if (!aUrl.startsWith("moz-action:"))
|
|
return null;
|
|
|
|
// URL is in the format moz-action:ACTION,PARAMS
|
|
// Where PARAMS is a JSON encoded object.
|
|
let [, type, params] = aUrl.match(/^moz-action:([^,]+),(.*)$/);
|
|
|
|
let action = {
|
|
type,
|
|
};
|
|
|
|
try {
|
|
action.params = JSON.parse(params);
|
|
for (let key in action.params) {
|
|
action.params[key] = decodeURIComponent(action.params[key]);
|
|
}
|
|
} catch (e) {
|
|
// If this failed, we assume that params is not a JSON object, and
|
|
// is instead just a flat string. This may happen for legacy
|
|
// search components.
|
|
action.params = {
|
|
url: params,
|
|
};
|
|
}
|
|
|
|
return action;
|
|
}
|
|
};
|
|
|
|
MozXULElement.implementCustomInterface(
|
|
MozElements.MozAutocompleteRichlistitem,
|
|
[Ci.nsIDOMXULSelectControlItemElement]
|
|
);
|
|
|
|
class MozAutocompleteRichlistitemInsecureWarning extends MozElements.MozAutocompleteRichlistitem {
|
|
constructor() {
|
|
super();
|
|
|
|
this.addEventListener("click", (event) => {
|
|
if (event.button != 0) {
|
|
return;
|
|
}
|
|
|
|
let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
|
|
window.openTrustedLinkIn(baseURL + "insecure-password", "tab", {
|
|
relatedToCurrent: true,
|
|
});
|
|
});
|
|
}
|
|
|
|
connectedCallback() {
|
|
if (this.delayConnectedCallback()) {
|
|
return;
|
|
}
|
|
|
|
super.connectedCallback();
|
|
|
|
// Unlike other autocomplete items, the height of the insecure warning
|
|
// increases by wrapping. So "forceHandleUnderflow" is for container to
|
|
// recalculate an item's height and width.
|
|
this.classList.add("forceHandleUnderflow");
|
|
}
|
|
|
|
static get observedAttributes() {
|
|
return [
|
|
"actiontype",
|
|
"current",
|
|
"selected",
|
|
"image",
|
|
"type",
|
|
];
|
|
}
|
|
|
|
get inheritedAttributeMap() {
|
|
if (!this.__inheritedAttributeMap) {
|
|
this.__inheritedAttributeMap = new Map([
|
|
[ this.querySelector(".ac-type-icon"), [ "selected", "current", "type" ] ],
|
|
[ this.querySelector(".ac-site-icon"), [ "src=image", "selected", "type" ] ],
|
|
[ this.querySelector(".ac-title-text"), [ "selected" ] ],
|
|
[ this.querySelector(".ac-tags-text"), [ "selected" ] ],
|
|
[ this.querySelector(".ac-separator"), [ "selected", "actiontype", "type" ] ],
|
|
[ this.querySelector(".ac-url"), [ "selected", "actiontype" ] ],
|
|
[ this.querySelector(".ac-url-text"), [ "selected" ] ],
|
|
[ this.querySelector(".ac-action"), [ "selected", "actiontype" ] ],
|
|
[ this.querySelector(".ac-action-text"), [ "selected" ] ],
|
|
]);
|
|
}
|
|
return this.__inheritedAttributeMap;
|
|
}
|
|
|
|
get _markup() {
|
|
return `
|
|
<image class="ac-type-icon"></image>
|
|
<image class="ac-site-icon"></image>
|
|
<vbox class="ac-title" align="left">
|
|
<description class="ac-text-overflow-container">
|
|
<description class="ac-title-text"></description>
|
|
</description>
|
|
</vbox>
|
|
<hbox class="ac-tags" align="center">
|
|
<description class="ac-text-overflow-container">
|
|
<description class="ac-tags-text"></description>
|
|
</description>
|
|
</hbox>
|
|
<hbox class="ac-separator" align="center">
|
|
<description class="ac-separator-text" value="—"></description>
|
|
</hbox>
|
|
<hbox class="ac-url" align="center">
|
|
<description class="ac-text-overflow-container">
|
|
<description class="ac-url-text"></description>
|
|
</description>
|
|
</hbox>
|
|
<hbox class="ac-action" align="center">
|
|
<description class="ac-text-overflow-container">
|
|
<description class="ac-action-text"></description>
|
|
</description>
|
|
</hbox>
|
|
`;
|
|
}
|
|
|
|
get _learnMoreString() {
|
|
if (!this.__learnMoreString) {
|
|
this.__learnMoreString = Services.strings.createBundle(
|
|
"chrome://passwordmgr/locale/passwordmgr.properties"
|
|
).
|
|
GetStringFromName("insecureFieldWarningLearnMore");
|
|
}
|
|
return this.__learnMoreString;
|
|
}
|
|
|
|
/**
|
|
* Override _getSearchTokens to have the Learn More text emphasized
|
|
*/
|
|
_getSearchTokens(aSearch) {
|
|
return [this._learnMoreString.toLowerCase()];
|
|
}
|
|
}
|
|
|
|
customElements.define("autocomplete-richlistitem", MozElements.MozAutocompleteRichlistitem, {
|
|
extends: "richlistitem",
|
|
});
|
|
|
|
customElements.define("autocomplete-richlistitem-insecure-warning", MozAutocompleteRichlistitemInsecureWarning, {
|
|
extends: "richlistitem",
|
|
});
|
|
}
|