/** * 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/. */ /* import-globals-from ../../../components/compose/content/addressingWidgetOverlay.js */ /* import-globals-from ../../../components/compose/content/MsgComposeCommands.js */ /* global MozElements */ /* global MozXULElement */ /* global gFolderDisplay */ /* global PluralForm */ /* global onRecipientsChanged */ // Wrap in a block to prevent leaking to window scope. { const { MailServices } = ChromeUtils.import( "resource:///modules/MailServices.jsm" ); const LazyModules = {}; ChromeUtils.defineModuleGetter( LazyModules, "DBViewWrapper", "resource:///modules/DBViewWrapper.jsm" ); ChromeUtils.defineModuleGetter( LazyModules, "MailUtils", "resource:///modules/MailUtils.jsm" ); ChromeUtils.defineModuleGetter( LazyModules, "MimeParser", "resource:///modules/mimeParser.jsm" ); ChromeUtils.defineModuleGetter( LazyModules, "TagUtils", "resource:///modules/TagUtils.jsm" ); // NOTE: Icon column headers should have their "label" attribute set to // describe the icon for the accessibility tree. // // NOTE: Ideally we could listen for the "alt" attribute and pass it on to the // contained , but the accessibility tree only seems to read the "label" // for a , and ignores the alt text. class MozTreecolImage extends customElements.get("treecol") { static get observedAttributes() { return ["src"]; } connectedCallback() { if (this.hasChildNodes() || this.delayConnectedCallback()) { return; } this.image = document.createElement("img"); this.image.classList.add("treecol-icon"); this.appendChild(this.image); this._updateAttributes(); } attributeChangedCallback() { this._updateAttributes(); } _updateAttributes() { if (!this.image) { return; } const src = this.getAttribute("src"); if (src != null) { this.image.setAttribute("src", src); } else { this.image.removeAttribute("src"); } } } customElements.define("treecol-image", MozTreecolImage, { extends: "treecol", }); /** * Class extending treecols. This features a customized treecolpicker that * features a menupopup with more items than the standard one. * * @augments {MozTreecols} */ class MozThreadPaneTreecols extends customElements.get("treecols") { connectedCallback() { if (this.delayConnectedCallback()) { return; } let treecolpicker = this.querySelector("treecolpicker:not([is]"); // Can't change the super treecolpicker by setting // is="thread-pane-treecolpicker" since that needs to be there at the // parsing stage to take effect. // So, remove the existing treecolpicker, and add a new one. if (treecolpicker) { treecolpicker.remove(); } if (!this.querySelector("treecolpicker[is=thread-pane-treecolpicker]")) { this.appendChild( MozXULElement.parseXULToFragment( ` `, ["chrome://messenger/locale/messenger.dtd"] ) ); } // Exceptionally apply super late, so we get the other goodness from there // now that the treecolpicker is corrected. super.connectedCallback(); } } customElements.define("thread-pane-treecols", MozThreadPaneTreecols, { extends: "treecols", }); /** * Class extending treecolpicker. This implements UI to apply column settings * of the current thread pane to other mail folders too. * * @augments {MozTreecolPicker} */ class MozThreadPaneTreeColpicker extends customElements.get("treecolpicker") { connectedCallback() { super.connectedCallback(); if (this.delayConnectedCallback()) { return; } MozXULElement.insertFTLIfNeeded("messenger/mailWidgets.ftl"); let popup = this.querySelector(`menupopup[anonid="popup"]`); // We'll add an "Apply columns to..." menu popup.appendChild( MozXULElement.parseXULToFragment( ` `, ["chrome://messenger/locale/messenger.dtd"] ) ); let confirmApplyCols = (destFolder, useChildren) => { // Confirm the action with the user. let bundle = document.getElementById("bundle_messenger"); let title = useChildren ? "threadPane.columnPicker.confirmFolder.withChildren.title" : "threadPane.columnPicker.confirmFolder.noChildren.title"; let message = useChildren ? "threadPane.columnPicker.confirmFolder.withChildren.message" : "threadPane.columnPicker.confirmFolder.noChildren.message"; let confirmed = Services.prompt.confirm( null, bundle.getString(title), bundle.getFormattedString(message, [destFolder.prettyName]) ); if (confirmed) { this._applyColumns(destFolder, useChildren); } }; this.querySelector(".applyToFolder-menu").addEventListener( "command", event => { confirmApplyCols(event.target._folder, false); } ); this.querySelector(".applyToFolderAndChildren-menu").addEventListener( "command", event => { confirmApplyCols(event.target._folder, true); } ); let confirmApplyView = async (destFolder, useChildren) => { let msgId = useChildren ? "threadpane-apply-changes-prompt-with-children-text" : "threadpane-apply-changes-prompt-no-children-text"; let [title, message] = await document.l10n.formatValues([ { id: "threadpane-apply-changes-prompt-title" }, { id: msgId, args: { name: destFolder.prettyName } }, ]); if (Services.prompt.confirm(null, title, message)) { this._applyView(destFolder, useChildren); } }; this.querySelector(".applyViewToFolder-menu").addEventListener( "command", event => { confirmApplyView(event.target._folder, false); } ); this.querySelector(".applyViewToFolderAndChildren-menu").addEventListener( "command", event => { confirmApplyView(event.target._folder, true); } ); } _applyColumns(destFolder, useChildren) { // Get the current folder's column state, plus the "swapped" column // state, which swaps "From" and "Recipient" if only one is shown. // This is useful for copying an incoming folder's columns to an // outgoing folder, or vice versa. let colState = gFolderDisplay.getColumnStates(); let myColStateString = JSON.stringify(colState); let swappedColStateString; if (colState.senderCol.visible != colState.recipientCol.visible) { let tmp = colState.senderCol; colState.senderCol = colState.recipientCol; colState.recipientCol = tmp; swappedColStateString = JSON.stringify(colState); } else { swappedColStateString = myColStateString; } let isOutgoing = function (folder) { return folder.isSpecialFolder( LazyModules.DBViewWrapper.prototype.OUTGOING_FOLDER_FLAGS, true ); }; let amIOutgoing = isOutgoing(gFolderDisplay.displayedFolder); let colStateString = function (folder) { return isOutgoing(folder) == amIOutgoing ? myColStateString : swappedColStateString; }; // Now propagate appropriately... const propName = gFolderDisplay.PERSISTED_COLUMN_PROPERTY_NAME; if (useChildren) { LazyModules.MailUtils.takeActionOnFolderAndDescendents( destFolder, folder => { folder.setStringProperty(propName, colStateString(folder)); // Force the reference to be forgotten. folder.msgDatabase = null; } ).then(() => { Services.obs.notifyObservers( gFolderDisplay.displayedFolder, "msg-folder-columns-propagated" ); }); } else { destFolder.setStringProperty(propName, colStateString(destFolder)); // null out to avoid memory bloat. destFolder.msgDatabase = null; } } _applyView(destFolder, useChildren) { let viewFlags = gFolderDisplay.displayedFolder.msgDatabase.dBFolderInfo.viewFlags; let sortType = gFolderDisplay.displayedFolder.msgDatabase.dBFolderInfo.sortType; let sortOrder = gFolderDisplay.displayedFolder.msgDatabase.dBFolderInfo.sortOrder; if (useChildren) { LazyModules.MailUtils.takeActionOnFolderAndDescendents( destFolder, folder => { folder.msgDatabase.dBFolderInfo.viewFlags = viewFlags; folder.msgDatabase.dBFolderInfo.sortType = sortType; folder.msgDatabase.dBFolderInfo.sortOrder = sortOrder; folder.msgDatabase = null; } ).then(() => { Services.obs.notifyObservers( gFolderDisplay.displayedFolder, "msg-folder-views-propagated" ); }); } else { destFolder.msgDatabase.dBFolderInfo.viewFlags = viewFlags; destFolder.msgDatabase.dBFolderInfo.sortType = sortType; destFolder.msgDatabase.dBFolderInfo.sortOrder = sortOrder; // null out to avoid memory bloat destFolder.msgDatabase = null; } } } customElements.define( "thread-pane-treecolpicker", MozThreadPaneTreeColpicker, { extends: "treecolpicker" } ); // The menulist CE is defined lazily. Create one now to get menulist defined, // allowing us to inherit from it. if (!customElements.get("menulist")) { delete document.createXULElement("menulist"); } { /** * MozMenulistEditable is a menulist widget that can be made editable by setting editable="true". * With an additional type="description" the list also contains an additional label that can hold * for instance, a description of a menu item. * It is typically used e.g. for the "Custom From Address..." feature to let the user chose and * edit the address to send from. * * @augments {MozMenuList} */ class MozMenulistEditable extends customElements.get("menulist") { static get markup() { // Accessibility information of these nodes will be // presented on XULComboboxAccessible generated from ; // hide these nodes from the accessibility tree. return ` `; } connectedCallback() { if (this.delayConnectedCallback()) { return; } this.shadowRoot.appendChild(this.constructor.fragment); this._inputField = this.shadowRoot.querySelector("input"); this._labelBox = this.shadowRoot.getElementById("label-box"); this._dropmarker = this.shadowRoot.querySelector("dropmarker"); if (this.getAttribute("type") == "description") { this._description = document.createXULElement("label"); this._description.id = this._description.part = "description"; this._description.setAttribute("crop", "end"); this._description.setAttribute("role", "none"); this.shadowRoot.getElementById("label").after(this._description); } this.initializeAttributeInheritance(); this.mSelectedInternal = null; this.setInitialSelection(); this._handleMutation = mutations => { this.editable = this.getAttribute("editable") == "true"; }; this.mAttributeObserver = new MutationObserver(this._handleMutation); this.mAttributeObserver.observe(this, { attributes: true, attributeFilter: ["editable"], }); this._keypress = event => { if (event.key == "ArrowDown") { this.open = true; } }; this._inputField.addEventListener("keypress", this._keypress); this._change = event => { event.stopPropagation(); this.selectedItem = null; this.setAttribute("value", this._inputField.value); // Start the event again, but this time with the menulist as target. this.dispatchEvent(new CustomEvent("change", { bubbles: true })); }; this._inputField.addEventListener("change", this._change); this._popupHiding = event => { // layerX is 0 if the user clicked outside the popup. if (this.editable && event.layerX > 0) { this._inputField.select(); } }; if (!this.menupopup) { this.appendChild(MozXULElement.parseXULToFragment(``)); } this.menupopup.addEventListener("popuphiding", this._popupHiding); } disconnectedCallback() { super.disconnectedCallback(); this.mAttributeObserver.disconnect(); this._inputField.removeEventListener("keypress", this._keypress); this._inputField.removeEventListener("change", this._change); this.menupopup.removeEventListener("popuphiding", this._popupHiding); for (let prop of [ "_inputField", "_labelBox", "_dropmarker", "_description", ]) { if (this[prop]) { this[prop].remove(); this[prop] = null; } } } static get inheritedAttributes() { let attrs = super.inheritedAttributes; attrs.input = "value,disabled"; attrs["#description"] = "value=description"; return attrs; } set editable(val) { if (val == this.editable) { return; } if (!val) { // If we were focused and transition from editable to not editable, // focus the parent menulist so that the focus does not get stuck. if (this._inputField == document.activeElement) { window.setTimeout(() => this.focus(), 0); } } this.setAttribute("editable", val); } get editable() { return this.getAttribute("editable") == "true"; } set value(val) { this._inputField.value = val; this.setAttribute("value", val); this.setAttribute("label", val); } get value() { if (this.editable) { return this._inputField.value; } return super.value; } get label() { if (this.editable) { return this._inputField.value; } return super.label; } set placeholder(val) { this._inputField.placeholder = val; } get placeholder() { return this._inputField.placeholder; } set selectedItem(val) { if (val) { this._inputField.value = val.getAttribute("value"); } super.selectedItem = val; } get selectedItem() { return super.selectedItem; } focus() { if (this.editable) { this._inputField.focus(); } else { super.focus(); } } select() { if (this.editable) { this._inputField.select(); } } } const MenuBaseControl = MozElements.BaseControlMixin( MozElements.MozElementMixin(XULMenuElement) ); MenuBaseControl.implementCustomInterface(MozMenulistEditable, [ Ci.nsIDOMXULMenuListElement, Ci.nsIDOMXULSelectControlElement, ]); customElements.define("menulist-editable", MozMenulistEditable, { extends: "menulist", }); } /** * The MozAttachmentlist widget lists attachments for a mail. This is typically used to show * attachments while writing a new mail as well as when reading mails. * * @augments {MozElements.RichListBox} */ class MozAttachmentlist extends MozElements.RichListBox { constructor() { super(); this.messenger = Cc["@mozilla.org/messenger;1"].createInstance( Ci.nsIMessenger ); this.addEventListener("keypress", event => { switch (event.key) { case " ": // Allow plain spacebar to select the focused item. if (!event.shiftKey && !event.ctrlKey) { this.addItemToSelection(this.currentItem); } // Prevent inbuilt scrolling. event.preventDefault(); break; case "Enter": if (this.currentItem && !event.ctrlKey && !event.shiftKey) { this.addItemToSelection(this.currentItem); let evt = document.createEvent("XULCommandEvent"); evt.initCommandEvent( "command", true, true, window, 0, event.ctrlKey, event.altKey, event.shiftKey, event.metaKey, null ); this.currentItem.dispatchEvent(evt); } break; } }); // Make sure we keep the focus. this.addEventListener("mousedown", event => { if (event.button != 0) { return; } if (document.commandDispatcher.focusedElement != this) { this.focus(); } }); } connectedCallback() { super.connectedCallback(); if (this.delayConnectedCallback()) { return; } let children = Array.from(this._childNodes); children .filter(child => child.getAttribute("selected") == "true") .forEach(this.selectedItems.append, this.selectedItems); children .filter(child => !child.hasAttribute("context")) .forEach(child => child.setAttribute("context", this.getAttribute("itemcontext")) ); } get itemCount() { return this._childNodes.length; } /** * Get the preferred height (the height that would allow us to fit * everything without scrollbars) of the attachmentlist's bounding * rectangle. Add 3px to account for item's margin. */ get preferredHeight() { return this.scrollHeight + this.getBoundingClientRect().height + 3; } get _childNodes() { return this.querySelectorAll("richlistitem.attachmentItem"); } getIndexOfItem(item) { for (let i = 0; i < this._childNodes.length; i++) { if (this._childNodes[i] === item) { return i; } } return -1; } getItemAtIndex(index) { if (index >= 0 && index < this._childNodes.length) { return this._childNodes[index]; } return null; } getRowCount() { return this._childNodes.length; } getIndexOfFirstVisibleRow() { if (this._childNodes.length == 0) { return -1; } // First try to estimate which row is visible, assuming they're all the same height. let box = this; let estimatedRow = Math.floor( box.scrollTop / this._childNodes[0].getBoundingClientRect().height ); let estimatedIndex = estimatedRow * this._itemsPerRow(); let offset = this._childNodes[estimatedIndex].screenY - box.screenY; if (offset > 0) { // We went too far! Go back until we find an item totally off-screen, then return the one // after that. for (let i = estimatedIndex - 1; i >= 0; i--) { let childBoxObj = this._childNodes[i].getBoundingClientRect(); if (childBoxObj.screenY + childBoxObj.height <= box.screenY) { return i + 1; } } // If we get here, we must have gone back to the beginning of the list, so just return 0. return 0; } // We didn't go far enough! Keep going until we find an item at least partially on-screen. for (let i = estimatedIndex; i < this._childNodes.length; i++) { let childBoxObj = this._childNodes[i].getBoundingClientRect(); if (childBoxObj.screenY + childBoxObj.height > box.screenY > 0) { return i; } } return null; } ensureIndexIsVisible(index) { this.ensureElementIsVisible(this.getItemAtIndex(index)); } ensureElementIsVisible(item) { let box = this; // Are we too far down? if (item.screenY < box.screenY) { box.scrollTop = item.getBoundingClientRect().y - box.getBoundingClientRect().y; } else if ( item.screenY + item.getBoundingClientRect().height > box.screenY + box.getBoundingClientRect().height ) { // ... or not far enough? box.scrollTop = item.getBoundingClientRect().y + item.getBoundingClientRect().height - box.getBoundingClientRect().y - box.getBoundingClientRect().height; } } scrollToIndex(index) { let box = this; let item = this.getItemAtIndex(index); if (!item) { return; } box.scrollTop = item.getBoundingClientRect().y - box.getBoundingClientRect().y; } appendItem(attachment, name) { // -1 appends due to the way getItemAtIndex is implemented. return this.insertItemAt(-1, attachment, name); } insertItemAt(index, attachment, name) { let item = this.ownerDocument.createXULElement("richlistitem"); item.classList.add("attachmentItem"); item.setAttribute("role", "option"); item.addEventListener("dblclick", event => { let evt = document.createEvent("XULCommandEvent"); evt.initCommandEvent( "command", true, true, window, 0, event.ctrlKey, event.altKey, event.shiftKey, event.metaKey, null ); item.dispatchEvent(evt); }); let makeDropIndicator = placementClass => { let img = document.createElement("img"); img.setAttribute( "src", "chrome://messenger/skin/icons/tab-drag-indicator.svg" ); img.setAttribute("alt", ""); img.classList.add("attach-drop-indicator", placementClass); return img; }; item.appendChild(makeDropIndicator("before")); let icon = this.ownerDocument.createElement("img"); icon.setAttribute("alt", ""); icon.setAttribute("draggable", "false"); // Allow the src to be invalid. icon.classList.add("attachmentcell-icon", "invisible-on-broken"); item.appendChild(icon); let textLabel = this.ownerDocument.createElement("span"); textLabel.classList.add("attachmentcell-name"); item.appendChild(textLabel); let extensionLabel = this.ownerDocument.createElement("span"); extensionLabel.classList.add("attachmentcell-extension"); item.appendChild(extensionLabel); let sizeLabel = this.ownerDocument.createElement("span"); sizeLabel.setAttribute("role", "note"); sizeLabel.classList.add("attachmentcell-size"); item.appendChild(sizeLabel); item.appendChild(makeDropIndicator("after")); item.setAttribute("context", this.getAttribute("itemcontext")); item.attachment = attachment; this.invalidateItem(item, name); this.insertBefore(item, this.getItemAtIndex(index)); return item; } /** * Set the attachment icon source. * * @param {MozRichlistitem} item - The attachment item to set the icon of. * @param {string|null} src - The src to set. */ setAttachmentIconSrc(item, src) { let icon = item.querySelector(".attachmentcell-icon"); icon.setAttribute("src", src); } /** * Refresh the attachment icon using the attachment details. * * @param {MozRichlistitem} item - The attachment item to refresh the icon * for. */ refreshAttachmentIcon(item) { let src; let attachment = item.attachment; let type = attachment.contentType; if (type == "text/x-moz-deleted") { src = "chrome://messenger/skin/icons/attachment-deleted.svg"; } else if (!item.loaded || item.uploading) { src = "chrome://global/skin/icons/loading.png"; } else if (item.cloudIcon) { src = item.cloudIcon; } else { let iconName = attachment.name; if (iconName.toLowerCase().endsWith(".eml")) { // Discard file names derived from subject headers with special // characters. iconName = "message.eml"; } else if (attachment.url) { // For local file urls, we are better off using the full file url // because moz-icon will actually resolve the file url and get the // right icon from the file url. All other urls, we should try to // extract the file name from them. This fixes issues where an icon // wasn't showing up if you dragged a web url that had a query or // reference string after the file name and for mailnews urls where // the filename is hidden in the url as a &filename= part. let url = Services.io.newURI(attachment.url); if ( url instanceof Ci.nsIURL && url.fileName && !url.schemeIs("file") ) { iconName = url.fileName; } } src = `moz-icon://${iconName}?size=16&contentType=${type}`; } this.setAttachmentIconSrc(item, src); } /** * Get whether the attachment list is fully loaded. * * @returns {boolean} - Whether all the attachments in the list are fully * loaded. */ isLoaded() { // Not loaded if at least one loading. for (let item of this.querySelectorAll(".attachmentItem")) { if (!item.loaded) { return false; } } return true; } /** * Set the attachment item's loaded state. * * @param {MozRichlistitem} item - The attachment item. * @param {boolean} loaded - Whether the attachment is fully loaded. */ setAttachmentLoaded(item, loaded) { item.loaded = loaded; this.refreshAttachmentIcon(item); } /** * Set the attachment item's cloud icon, if any. * * @param {MozRichlistitem} item - The attachment item. * @param {?string} cloudIcon - The icon of the cloud provider where the * attachment was uploaded. Will be used as file type icon in the list of * attachments, if specified. */ setCloudIcon(item, cloudIcon) { item.cloudIcon = cloudIcon; this.refreshAttachmentIcon(item); } /** * Set the attachment item's displayed name. * * @param {MozRichlistitem} item - The attachment item. * @param {string} name - The name to display for the attachment. */ setAttachmentName(item, name) { item.setAttribute("name", name); // Extract what looks like the file extension so we can always show it, // even if the full name would overflow. // NOTE: This is a convenience feature rather than a security feature // since the content type of an attachment need not match the extension. let found = name.match(/^(.+)(\.[a-zA-Z0-9_#$!~+-]{1,16})$/); item.querySelector(".attachmentcell-name").textContent = found?.[1] || name; item.querySelector(".attachmentcell-extension").textContent = found?.[2] || ""; } /** * Set the attachment item's displayed size. * * @param {MozRichlistitem} item - The attachment item. * @param {string} size - The size to display for the attachment. */ setAttachmentSize(item, size) { item.setAttribute("size", size); let sizeEl = item.querySelector(".attachmentcell-size"); sizeEl.textContent = size; sizeEl.hidden = !size; } invalidateItem(item, name) { let attachment = item.attachment; this.setAttachmentName(item, name || attachment.name); let size = attachment.size == null || attachment.size == -1 ? "" : this.messenger.formatFileSize(attachment.size); if (size && item.cloudHtmlFileSize > 0) { size = `${this.messenger.formatFileSize( item.cloudHtmlFileSize )} (${size})`; } this.setAttachmentSize(item, size); // By default, items are considered loaded. item.loaded = true; this.refreshAttachmentIcon(item); return item; } /** * Find the attachmentitem node for the specified nsIMsgAttachment. */ findItemForAttachment(aAttachment) { for (let i = 0; i < this.itemCount; i++) { let item = this.getItemAtIndex(i); if (item.attachment == aAttachment) { return item; } } return null; } _fireOnSelect() { if (!this._suppressOnSelect && !this.suppressOnSelect) { this.dispatchEvent( new Event("select", { bubbles: false, cancelable: true }) ); } } _itemsPerRow() { // For 0 or 1 children, we can assume that they all fit in one row. if (this._childNodes.length < 2) { return this._childNodes.length; } let itemWidth = this._childNodes[1].getBoundingClientRect().x - this._childNodes[0].getBoundingClientRect().x; // Each item takes up a full row if (itemWidth == 0) { return 1; } return Math.floor(this.clientWidth / itemWidth); } _itemsPerCol(aItemsPerRow) { let itemsPerRow = aItemsPerRow || this._itemsPerRow(); if (this._childNodes.length == 0) { return 0; } if (this._childNodes.length <= itemsPerRow) { return 1; } let itemHeight = this._childNodes[itemsPerRow].getBoundingClientRect().y - this._childNodes[0].getBoundingClientRect().y; return Math.floor(this.clientHeight / itemHeight); } /** * Set the width of each child to the largest width child to create a * grid-like effect for the flex-wrapped attachment list. */ setOptimumWidth() { if (this._childNodes.length == 0) { return; } let width = 0; for (let child of this._childNodes) { // Unset the width, then the child will expand or shrink to its // "natural" size in the flex-wrapped container. I.e. its preferred // width bounded by the width of the container's content space. child.style.width = null; width = Math.max(width, child.getBoundingClientRect().width); } for (let child of this._childNodes) { child.style.width = `${width}px`; } } } customElements.define("attachment-list", MozAttachmentlist, { extends: "richlistbox", }); /** * The MailAddressPill widget is used to display the email addresses in the * messengercompose.xhtml window. * * @augments {MozXULElement} */ class MailAddressPill extends MozXULElement { static get inheritedAttributes() { return { ".pill-label": "crop,value=label", }; } /** * Indicates whether the address of this pill is for a mail list. * * @type {boolean} */ isMailList = false; /** * If this pill is for a mail list, this provides the URI. * * @type {?string} */ listURI = null; /** * If this pill is for a mail list, this provides the total count of * its addresses. * * @type {number} */ listAddressCount = 0; connectedCallback() { if (this.hasChildNodes() || this.delayConnectedCallback()) { return; } this.classList.add("address-pill"); this.setAttribute("context", "emailAddressPillPopup"); this.setAttribute("allowevents", "true"); this.labelView = document.createXULElement("hbox"); this.labelView.setAttribute("flex", "1"); this.pillLabel = document.createXULElement("label"); this.pillLabel.classList.add("pill-label"); this.pillLabel.setAttribute("crop", "center"); this.pillIndicator = document.createElement("img"); this.pillIndicator.setAttribute( "src", "chrome://messenger/skin/icons/pill-indicator.svg" ); this.pillIndicator.setAttribute("alt", ""); this.pillIndicator.classList.add("pill-indicator"); this.pillIndicator.hidden = true; this.labelView.appendChild(this.pillLabel); this.labelView.appendChild(this.pillIndicator); this.appendChild(this.labelView); this._setupEmailInput(); this._setupEventListeners(); this.initializeAttributeInheritance(); // @implements {nsIObserver} this.inputObserver = { observe: (subject, topic, data) => { if (topic == "autocomplete-did-enter-text" && this.isEditing) { this.updatePill(); } }, }; Services.obs.addObserver( this.inputObserver, "autocomplete-did-enter-text" ); // Remove the observer on window unload as the disconnectedCallback() // will never be called when closing a window, so we might therefore // leak if XPCOM isn't smart enough. window.addEventListener( "unload", () => { this.removeObserver(); }, { once: true } ); } get emailAddress() { return this.getAttribute("emailAddress"); } set emailAddress(val) { this.setAttribute("emailAddress", val); } get label() { return this.getAttribute("label"); } set label(val) { this.setAttribute("label", val); } get fullAddress() { return this.getAttribute("fullAddress"); } set fullAddress(val) { this.setAttribute("fullAddress", val); } get displayName() { return this.getAttribute("displayName"); } set displayName(val) { this.setAttribute("displayName", val); } get emailInput() { return this.querySelector(`input[is="autocomplete-input"]`); } /** * Get the main addressing input field the pill belongs to. */ get rowInput() { return this.closest(".address-container").querySelector( ".address-row-input" ); } /** * Check if the pill is currently in "Edit Mode", meaning the label is * hidden and the html:input field is visible. * * @returns {boolean} true if the pill is currently being edited. */ get isEditing() { return !this.emailInput.hasAttribute("hidden"); } get fragment() { if (!this.constructor.hasOwnProperty("_fragment")) { this.constructor._fragment = MozXULElement.parseXULToFragment(`