999 lines
30 KiB
JavaScript
999 lines
30 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 { LoginHelper } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/LoginHelper.sys.mjs"
|
|
);
|
|
|
|
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", () => {
|
|
// 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(this.constructor.fragment);
|
|
this.initializeAttributeInheritance();
|
|
|
|
this._boundaryCutoff = null;
|
|
this._inOverflow = false;
|
|
|
|
this._adjustAcItem();
|
|
}
|
|
|
|
static get inheritedAttributes() {
|
|
return {
|
|
".ac-type-icon": "selected,current,type",
|
|
".ac-site-icon": "src=image,selected,type",
|
|
".ac-title": "selected",
|
|
".ac-title-text": "selected",
|
|
".ac-separator": "selected,type",
|
|
".ac-url": "selected",
|
|
".ac-url-text": "selected",
|
|
};
|
|
}
|
|
|
|
static get markup() {
|
|
return `
|
|
<image class="ac-type-icon"/>
|
|
<image class="ac-site-icon"/>
|
|
<hbox class="ac-title" align="center">
|
|
<description class="ac-text-overflow-container">
|
|
<description class="ac-title-text"/>
|
|
</description>
|
|
</hbox>
|
|
<hbox class="ac-separator" align="center">
|
|
<description class="ac-separator-text" value="—"/>
|
|
</hbox>
|
|
<hbox class="ac-url" align="center" aria-hidden="true">
|
|
<description class="ac-text-overflow-container">
|
|
<description class="ac-url-text"/>
|
|
</description>
|
|
</hbox>
|
|
`;
|
|
}
|
|
|
|
get _typeIcon() {
|
|
return this.querySelector(".ac-type-icon");
|
|
}
|
|
|
|
get _titleText() {
|
|
return this.querySelector(".ac-title-text");
|
|
}
|
|
|
|
get _separator() {
|
|
return this.querySelector(".ac-separator");
|
|
}
|
|
|
|
get _urlText() {
|
|
return this.querySelector(".ac-url-text");
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
_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) {
|
|
// Get rid of all previous text
|
|
if (!aDescriptionElement) {
|
|
return;
|
|
}
|
|
while (aDescriptionElement.hasChildNodes()) {
|
|
aDescriptionElement.firstChild.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);
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
_setUpEmphasisSpan(aSpan, aDescriptionElement) {
|
|
aSpan.classList.add("ac-emphasize-text");
|
|
switch (aDescriptionElement) {
|
|
case this._titleText:
|
|
aSpan.classList.add("ac-emphasize-text-title");
|
|
break;
|
|
case this._urlText:
|
|
aSpan.classList.add("ac-emphasize-text-url");
|
|
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(url);
|
|
}
|
|
|
|
_reuseAcItem() {
|
|
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();
|
|
}
|
|
}
|
|
|
|
_adjustAcItem() {
|
|
let originalUrl = this.getAttribute("ac-value");
|
|
let title = this.getAttribute("ac-label");
|
|
this.setAttribute("url", originalUrl);
|
|
this.setAttribute("image", this.getAttribute("ac-image"));
|
|
this.setAttribute("title", title);
|
|
this.setAttribute("text", this.getAttribute("ac-text"));
|
|
|
|
let type = this.getAttribute("originaltype");
|
|
let types = new Set(type.split(/\s+/));
|
|
// Remove types that should ultimately not be in the `type` string.
|
|
types.delete("autofill");
|
|
type = [...types][0] || "";
|
|
this.setAttribute("type", type);
|
|
|
|
let displayUrl = this._unescapeUrl(originalUrl);
|
|
|
|
// Show the domain as the title if we don't have a title.
|
|
if (!title) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (Array.isArray(title)) {
|
|
this._setUpEmphasisedSections(this._titleText, title);
|
|
} else {
|
|
this._setUpDescription(this._titleText, title);
|
|
}
|
|
this._setUpDescription(this._urlText, displayUrl);
|
|
|
|
// 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._urlText.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 separatorRect = this._separator.getBoundingClientRect();
|
|
let urlRect = this._urlText.getBoundingClientRect();
|
|
let separatorURLWidth = separatorRect.width + urlRect.width;
|
|
|
|
// Total width for the title and URL 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);
|
|
|
|
let titleWidth = titleRect.width;
|
|
if (titleWidth + separatorURLWidth > itemWidth) {
|
|
// The percentage of the item width allocated to the title.
|
|
let titlePct = 0.66;
|
|
|
|
let titleAvailable = itemWidth - separatorURLWidth;
|
|
let titleMaxWidth = Math.max(titleAvailable, itemWidth * titlePct);
|
|
if (titleWidth > titleMaxWidth) {
|
|
this._titleText.style.maxWidth = titleMaxWidth + "px";
|
|
}
|
|
let urlMaxWidth = Math.max(
|
|
itemWidth - titleWidth,
|
|
itemWidth * (1 - titlePct)
|
|
);
|
|
urlMaxWidth -= separatorRect.width;
|
|
this._urlText.style.maxWidth = urlMaxWidth + "px";
|
|
this._hasMaxWidths = true;
|
|
}
|
|
}
|
|
|
|
handleOverUnderflow() {
|
|
this._removeMaxWidths();
|
|
this._handleOverflow();
|
|
}
|
|
};
|
|
|
|
MozXULElement.implementCustomInterface(
|
|
MozElements.MozAutocompleteRichlistitem,
|
|
[Ci.nsIDOMXULSelectControlItemElement]
|
|
);
|
|
|
|
class MozAutocompleteRichlistitemInsecureWarning extends MozElements.MozAutocompleteRichlistitem {
|
|
constructor() {
|
|
super();
|
|
|
|
this.addEventListener("click", event => {
|
|
if (event.button != 0) {
|
|
return;
|
|
}
|
|
|
|
let comment = this.getAttribute("ac-comment");
|
|
if (comment && JSON.parse(comment)?.noLearnMore) {
|
|
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 inheritedAttributes() {
|
|
return {
|
|
".ac-type-icon": "selected,current,type",
|
|
".ac-site-icon": "src=image,selected,type",
|
|
".ac-title-text": "selected",
|
|
".ac-separator": "selected,type",
|
|
".ac-url": "selected",
|
|
".ac-url-text": "selected",
|
|
};
|
|
}
|
|
|
|
static get markup() {
|
|
return `
|
|
<image class="ac-type-icon"/>
|
|
<image class="ac-site-icon"/>
|
|
<vbox class="ac-title" align="left">
|
|
<description class="ac-text-overflow-container">
|
|
<description class="ac-title-text"/>
|
|
</description>
|
|
</vbox>
|
|
<hbox class="ac-separator" align="center">
|
|
<description class="ac-separator-text" value="—"/>
|
|
</hbox>
|
|
<hbox class="ac-url" align="center">
|
|
<description class="ac-text-overflow-container">
|
|
<description class="ac-url-text"/>
|
|
</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() {
|
|
return [this._learnMoreString.toLowerCase()];
|
|
}
|
|
}
|
|
|
|
class MozAutocompleteRichlistitemLoginsFooter extends MozElements.MozAutocompleteRichlistitem {}
|
|
|
|
class MozAutocompleteImportableLearnMoreRichlistitem extends MozElements.MozAutocompleteRichlistitem {
|
|
constructor() {
|
|
super();
|
|
MozXULElement.insertFTLIfNeeded("toolkit/main-window/autocomplete.ftl");
|
|
}
|
|
|
|
static get markup() {
|
|
return `
|
|
<image class="ac-type-icon"/>
|
|
<image class="ac-site-icon"/>
|
|
<vbox class="ac-title" align="left">
|
|
<description class="ac-text-overflow-container">
|
|
<description class="ac-title-text"
|
|
data-l10n-id="autocomplete-import-learn-more"/>
|
|
</description>
|
|
</vbox>
|
|
<hbox class="ac-separator" align="center">
|
|
<description class="ac-separator-text" value="—"/>
|
|
</hbox>
|
|
<hbox class="ac-url" align="center">
|
|
<description class="ac-text-overflow-container">
|
|
<description class="ac-url-text"/>
|
|
</description>
|
|
</hbox>
|
|
`;
|
|
}
|
|
|
|
// Override to avoid clearing out fluent description.
|
|
_setUpDescription() {}
|
|
}
|
|
|
|
class MozAutocompleteTwoLineRichlistitem extends MozElements.MozRichlistitem {
|
|
connectedCallback() {
|
|
if (this.delayConnectedCallback()) {
|
|
return;
|
|
}
|
|
|
|
this.textContent = "";
|
|
this.appendChild(this.constructor.fragment);
|
|
this.initializeAttributeInheritance();
|
|
this.initializeSecondaryAction();
|
|
this._adjustAcItem();
|
|
}
|
|
|
|
initializeSecondaryAction() {
|
|
const button = this.querySelector(".ac-secondary-action");
|
|
|
|
if (this.onSecondaryAction) {
|
|
button.addEventListener("mousedown", event => {
|
|
event.stopPropagation();
|
|
this.onSecondaryAction();
|
|
});
|
|
} else {
|
|
button?.remove();
|
|
}
|
|
}
|
|
|
|
static get inheritedAttributes() {
|
|
return {
|
|
// getLabelAt:
|
|
".line1-label": "text=ac-label",
|
|
".ac-site-icon": "src=ac-image",
|
|
};
|
|
}
|
|
|
|
static get markup() {
|
|
return `
|
|
<div xmlns="http://www.w3.org/1999/xhtml"
|
|
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
|
class="two-line-wrapper">
|
|
<xul:image class="ac-site-icon"></xul:image>
|
|
<div class="labels-wrapper">
|
|
<div class="label-row line1-label"></div>
|
|
<div class="label-row line2-label"></div>
|
|
</div>
|
|
<button class="ac-secondary-action"></button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
_adjustAcItem() {
|
|
let comment = JSON.parse(this.getAttribute("ac-comment"));
|
|
this.querySelector(".line2-label").textContent = comment?.secondary || "";
|
|
|
|
this.querySelector(".ac-site-icon").collapsed =
|
|
this.getAttribute("ac-image") == "";
|
|
}
|
|
|
|
_onOverflow() {}
|
|
|
|
_onUnderflow() {}
|
|
|
|
handleOverUnderflow() {}
|
|
}
|
|
|
|
class MozAutocompleteLoginRichlistitem extends MozAutocompleteTwoLineRichlistitem {
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
this.firstChild.classList.add("ac-login-item");
|
|
}
|
|
|
|
onSecondaryAction() {
|
|
const comment = JSON.parse(this.getAttribute("ac-comment"));
|
|
LoginHelper.openPasswordManager(window, {
|
|
loginGuid: comment?.guid,
|
|
});
|
|
}
|
|
|
|
static get inheritedAttributes() {
|
|
return {
|
|
// getLabelAt:
|
|
".line1-label": "text=ac-label",
|
|
".ac-site-icon": "src=ac-image",
|
|
};
|
|
}
|
|
}
|
|
|
|
// This type has an action that is triggered when activated. The comment
|
|
// for that result should contain a fillMessageName which is the message to send.
|
|
class MozAutocompleteActionRichlistitem extends MozAutocompleteTwoLineRichlistitem {
|
|
constructor() {
|
|
super();
|
|
this.selectedByMouseOver = true;
|
|
}
|
|
}
|
|
|
|
// A row that conveys status information assigned from the status field
|
|
// within the comment associated with the selected item in the list.
|
|
class MozAutocompleteStatusRichlistitem extends MozAutocompleteTwoLineRichlistitem {
|
|
static get markup() {
|
|
return `<div class="ac-status" xmlns="http://www.w3.org/1999/xhtml"></div>`;
|
|
}
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
this.parentNode.addEventListener("select", this);
|
|
this.eventListenerParentNode = this.parentNode;
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
this.eventListenerParentNode?.removeEventListener("select", this);
|
|
this.eventListenerParentNode = null;
|
|
}
|
|
|
|
handleEvent(event) {
|
|
if (event.type == "select") {
|
|
let selectedItem = event.target.selectedItem;
|
|
if (selectedItem) {
|
|
this.#setStatus(selectedItem);
|
|
}
|
|
}
|
|
}
|
|
|
|
#setStatus(item) {
|
|
// For normal rows, use that row's comment, otherwise use the status's
|
|
// comment which serves as the default label.
|
|
let target =
|
|
!item || item instanceof MozAutocompleteActionRichlistitem
|
|
? this
|
|
: item;
|
|
|
|
let comment = JSON.parse(target.getAttribute("ac-comment"));
|
|
|
|
let statusBox = this.querySelector(".ac-status");
|
|
statusBox.textContent = comment?.status || "";
|
|
}
|
|
|
|
_adjustAcItem() {
|
|
this.#setStatus(this);
|
|
this.setAttribute("disabled", "true");
|
|
}
|
|
}
|
|
|
|
class MozAutocompleteAutoFillRichlistitem extends MozAutocompleteTwoLineRichlistitem {
|
|
constructor() {
|
|
super();
|
|
this.selectedByMouseOver = true;
|
|
}
|
|
|
|
_adjustAcItem() {
|
|
let label = this.getAttribute("ac-label");
|
|
this.querySelector(".line1-label").textContent = label;
|
|
|
|
let { secondary, ariaLabel } = JSON.parse(
|
|
this.getAttribute("ac-comment")
|
|
);
|
|
|
|
let line2Label = this.querySelector(".line2-label");
|
|
line2Label.textContent = secondary ?? "";
|
|
|
|
if (ariaLabel) {
|
|
this.setAttribute("aria-label", ariaLabel);
|
|
}
|
|
|
|
this.querySelector(".ac-site-icon").collapsed =
|
|
this.getAttribute("ac-image") == "";
|
|
}
|
|
|
|
set selected(val) {
|
|
if (val) {
|
|
this.setAttribute("selected", "true");
|
|
} else {
|
|
this.removeAttribute("selected");
|
|
}
|
|
|
|
setTimeout(() => {
|
|
const { AutoCompleteParent } = ChromeUtils.importESModule(
|
|
"resource://gre/actors/AutoCompleteParent.sys.mjs"
|
|
);
|
|
const actor = AutoCompleteParent.getCurrentActor();
|
|
actor?.previewAutoCompleteEntry();
|
|
}, 0);
|
|
}
|
|
|
|
get selected() {
|
|
return this.getAttribute("selected") == "true";
|
|
}
|
|
}
|
|
|
|
class MozAutocompleteGeneratedPasswordRichlistitem extends MozAutocompleteTwoLineRichlistitem {
|
|
constructor() {
|
|
super();
|
|
|
|
// Line 2 and line 3 both display text with a different line-height than
|
|
// line 1 but we want the line-height to be the same so we wrap the text
|
|
// in <span> and only adjust the line-height via font CSS properties on them.
|
|
this.generatedPasswordText = document.createElement("span");
|
|
|
|
this.line3Text = document.createElement("span");
|
|
this.line3 = document.createElement("div");
|
|
this.line3.className = "label-row generated-password-autosave";
|
|
this.line3.append(this.line3Text);
|
|
}
|
|
|
|
get _autoSaveString() {
|
|
if (!this.__autoSaveString) {
|
|
let brandShorterName = Services.strings
|
|
.createBundle("chrome://branding/locale/brand.properties")
|
|
.GetStringFromName("brandShorterName");
|
|
this.__autoSaveString = Services.strings
|
|
.createBundle("chrome://passwordmgr/locale/passwordmgr.properties")
|
|
.formatStringFromName("generatedPasswordWillBeSaved", [
|
|
brandShorterName,
|
|
]);
|
|
}
|
|
return this.__autoSaveString;
|
|
}
|
|
|
|
_adjustAcItem() {
|
|
let { generatedPassword, willAutoSaveGeneratedPassword } = JSON.parse(
|
|
this.getAttribute("ac-comment")
|
|
);
|
|
let line2Label = this.querySelector(".line2-label");
|
|
line2Label.textContent = "";
|
|
this.generatedPasswordText.textContent = generatedPassword;
|
|
line2Label.append(this.generatedPasswordText);
|
|
|
|
if (willAutoSaveGeneratedPassword) {
|
|
this.line3Text.textContent = this._autoSaveString;
|
|
this.querySelector(".labels-wrapper").append(this.line3);
|
|
} else {
|
|
this.line3.remove();
|
|
}
|
|
|
|
super._adjustAcItem();
|
|
}
|
|
}
|
|
|
|
class MozAutocompleteImportableLoginsRichlistitem extends MozAutocompleteTwoLineRichlistitem {
|
|
constructor() {
|
|
super();
|
|
MozXULElement.insertFTLIfNeeded("toolkit/main-window/autocomplete.ftl");
|
|
}
|
|
|
|
static get inheritedAttributes() {
|
|
return {
|
|
// getLabelAt:
|
|
".line1-label": "text=ac-label",
|
|
};
|
|
}
|
|
|
|
static get markup() {
|
|
return `
|
|
<div xmlns="http://www.w3.org/1999/xhtml"
|
|
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
|
class="two-line-wrapper">
|
|
<xul:image class="ac-site-icon" />
|
|
<div class="labels-wrapper">
|
|
<div class="label-row line1-label" data-l10n-name="line1" />
|
|
<div class="label-row line2-label" data-l10n-name="line2" />
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
_adjustAcItem() {
|
|
super._adjustAcItem();
|
|
document.l10n.setAttributes(
|
|
this.querySelector(".labels-wrapper"),
|
|
`autocomplete-import-logins-${this.getAttribute("ac-value")}`,
|
|
{
|
|
host: JSON.parse(this.getAttribute("ac-comment")).hostname.replace(
|
|
/^www\./,
|
|
""
|
|
),
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
customElements.define(
|
|
"autocomplete-richlistitem",
|
|
MozElements.MozAutocompleteRichlistitem,
|
|
{
|
|
extends: "richlistitem",
|
|
}
|
|
);
|
|
|
|
customElements.define(
|
|
"autocomplete-richlistitem-insecure-warning",
|
|
MozAutocompleteRichlistitemInsecureWarning,
|
|
{
|
|
extends: "richlistitem",
|
|
}
|
|
);
|
|
|
|
customElements.define(
|
|
"autocomplete-richlistitem-logins-footer",
|
|
MozAutocompleteRichlistitemLoginsFooter,
|
|
{
|
|
extends: "richlistitem",
|
|
}
|
|
);
|
|
|
|
customElements.define(
|
|
"autocomplete-two-line-richlistitem",
|
|
MozAutocompleteTwoLineRichlistitem,
|
|
{
|
|
extends: "richlistitem",
|
|
}
|
|
);
|
|
|
|
customElements.define(
|
|
"autocomplete-autofill-richlistitem",
|
|
MozAutocompleteAutoFillRichlistitem,
|
|
{
|
|
extends: "richlistitem",
|
|
}
|
|
);
|
|
|
|
customElements.define(
|
|
"autocomplete-login-richlistitem",
|
|
MozAutocompleteLoginRichlistitem,
|
|
{
|
|
extends: "richlistitem",
|
|
}
|
|
);
|
|
|
|
customElements.define(
|
|
"autocomplete-action-richlistitem",
|
|
MozAutocompleteActionRichlistitem,
|
|
{
|
|
extends: "richlistitem",
|
|
}
|
|
);
|
|
|
|
customElements.define(
|
|
"autocomplete-status-richlistitem",
|
|
MozAutocompleteStatusRichlistitem,
|
|
{
|
|
extends: "richlistitem",
|
|
}
|
|
);
|
|
|
|
customElements.define(
|
|
"autocomplete-generated-password-richlistitem",
|
|
MozAutocompleteGeneratedPasswordRichlistitem,
|
|
{
|
|
extends: "richlistitem",
|
|
}
|
|
);
|
|
|
|
customElements.define(
|
|
"autocomplete-importable-learn-more-richlistitem",
|
|
MozAutocompleteImportableLearnMoreRichlistitem,
|
|
{
|
|
extends: "richlistitem",
|
|
}
|
|
);
|
|
|
|
customElements.define(
|
|
"autocomplete-importable-logins-richlistitem",
|
|
MozAutocompleteImportableLoginsRichlistitem,
|
|
{
|
|
extends: "richlistitem",
|
|
}
|
|
);
|
|
}
|