diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/search/content/searchWidgets.js | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | comm/mailnews/search/content/searchWidgets.js | 1779 |
1 files changed, 1779 insertions, 0 deletions
diff --git a/comm/mailnews/search/content/searchWidgets.js b/comm/mailnews/search/content/searchWidgets.js new file mode 100644 index 0000000000..add3ed29b8 --- /dev/null +++ b/comm/mailnews/search/content/searchWidgets.js @@ -0,0 +1,1779 @@ +/** + * 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/. */ + +/* global MozElements MozXULElement */ + +/* import-globals-from ../../base/content/dateFormat.js */ +// TODO: This is completely bogus. Only one use of this file also has FilterEditor.js. +/* import-globals-from FilterEditor.js */ + +// Wrap in a block to prevent leaking to window scope. +{ + const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" + ); + const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); + + const updateParentNode = parentNode => { + if (parentNode.hasAttribute("initialActionIndex")) { + let actionIndex = parentNode.getAttribute("initialActionIndex"); + let filterAction = gFilter.getActionAt(actionIndex); + parentNode.initWithAction(filterAction); + } + parentNode.updateRemoveButton(); + }; + + class MozRuleactiontargetTag extends MozXULElement { + connectedCallback() { + const menulist = document.createXULElement("menulist"); + const menuPopup = document.createXULElement("menupopup"); + + menulist.classList.add("ruleactionitem"); + menulist.setAttribute("flex", "1"); + menulist.appendChild(menuPopup); + + for (let taginfo of MailServices.tags.getAllTags()) { + const newMenuItem = document.createXULElement("menuitem"); + newMenuItem.setAttribute("label", taginfo.tag); + newMenuItem.setAttribute("value", taginfo.key); + if (taginfo.color) { + newMenuItem.setAttribute("style", `color: ${taginfo.color};`); + } + menuPopup.appendChild(newMenuItem); + } + + this.appendChild(menulist); + + updateParentNode(this.closest(".ruleaction")); + } + } + + class MozRuleactiontargetPriority extends MozXULElement { + connectedCallback() { + this.appendChild( + MozXULElement.parseXULToFragment( + ` + <menulist class="ruleactionitem" flex="1"> + <menupopup> + <menuitem value="6" label="&highestPriorityCmd.label;"></menuitem> + <menuitem value="5" label="&highPriorityCmd.label;"></menuitem> + <menuitem value="4" label="&normalPriorityCmd.label;"></menuitem> + <menuitem value="3" label="&lowPriorityCmd.label;"></menuitem> + <menuitem value="2" label="&lowestPriorityCmd.label;"></menuitem> + </menupopup> + </menulist> + `, + ["chrome://messenger/locale/FilterEditor.dtd"] + ) + ); + + updateParentNode(this.closest(".ruleaction")); + } + } + + class MozRuleactiontargetJunkscore extends MozXULElement { + connectedCallback() { + this.appendChild( + MozXULElement.parseXULToFragment( + ` + <menulist class="ruleactionitem" flex="1"> + <menupopup> + <menuitem value="100" label="&junk.label;"/> + <menuitem value="0" label="¬Junk.label;"/> + </menupopup> + </menulist> + `, + ["chrome://messenger/locale/FilterEditor.dtd"] + ) + ); + + updateParentNode(this.closest(".ruleaction")); + } + } + + class MozRuleactiontargetReplyto extends MozXULElement { + connectedCallback() { + const menulist = document.createXULElement("menulist"); + const menuPopup = document.createXULElement("menupopup"); + + menulist.classList.add("ruleactionitem"); + menulist.setAttribute("flex", "1"); + menulist.appendChild(menuPopup); + + this.appendChild(menulist); + + let ruleaction = this.closest(".ruleaction"); + let raMenulist = ruleaction.querySelector( + '[is="ruleactiontype-menulist"]' + ); + for (let { label, value } of raMenulist.findTemplates()) { + menulist.appendItem(label, value); + } + updateParentNode(ruleaction); + } + } + + class MozRuleactiontargetForwardto extends MozXULElement { + connectedCallback() { + const input = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "input" + ); + input.classList.add("ruleactionitem", "input-inline"); + + this.classList.add("input-container"); + this.appendChild(input); + + updateParentNode(this.closest(".ruleaction")); + } + } + + class MozRuleactiontargetFolder extends MozXULElement { + connectedCallback() { + this.appendChild( + MozXULElement.parseXULToFragment( + ` + <menulist class="ruleactionitem + folderMenuItem" + flex="1" + displayformat="verbose"> + <menupopup is="folder-menupopup" + mode="filing" + class="menulist-menupopup" + showRecent="true" + recentLabel="&recentFolders.label;" + showFileHereLabel="true"> + </menupopup> + </menulist> + `, + ["chrome://messenger/locale/messenger.dtd"] + ) + ); + + this.menulist = this.querySelector("menulist"); + + this.menulist.addEventListener("command", event => { + this.setPicker(event); + }); + + updateParentNode(this.closest(".ruleaction")); + + let folder = this.menulist.value + ? MailUtils.getOrCreateFolder(this.menulist.value) + : gFilterList.folder; + + // An account folder is not a move/copy target; show "Choose Folder". + folder = folder.isServer ? null : folder; + + this.menulist.menupopup.selectFolder(folder); + } + + setPicker(event) { + this.menulist.menupopup.selectFolder(event.target._folder); + } + } + + class MozRuleactiontargetWrapper extends MozXULElement { + static get observedAttributes() { + return ["type"]; + } + + get ruleactiontargetElement() { + return this.node; + } + + connectedCallback() { + this._updateAttributes(); + } + + attributeChangedCallback() { + this._updateAttributes(); + } + + _getChildNode(type) { + const elementMapping = { + movemessage: "ruleactiontarget-folder", + copymessage: "ruleactiontarget-folder", + setpriorityto: "ruleactiontarget-priority", + setjunkscore: "ruleactiontarget-junkscore", + forwardmessage: "ruleactiontarget-forwardto", + replytomessage: "ruleactiontarget-replyto", + addtagtomessage: "ruleactiontarget-tag", + }; + const elementName = elementMapping[type]; + + return elementName ? document.createXULElement(elementName) : null; + } + + _updateAttributes() { + if (!this.hasAttribute("type")) { + return; + } + + const type = this.getAttribute("type"); + + while (this.lastChild) { + this.lastChild.remove(); + } + + if (type == null) { + return; + } + + this.node = this._getChildNode(type); + + if (this.node) { + this.node.setAttribute("flex", "1"); + this.appendChild(this.node); + } else { + updateParentNode(this.closest(".ruleaction")); + } + } + } + + customElements.define("ruleactiontarget-tag", MozRuleactiontargetTag); + customElements.define( + "ruleactiontarget-priority", + MozRuleactiontargetPriority + ); + customElements.define( + "ruleactiontarget-junkscore", + MozRuleactiontargetJunkscore + ); + customElements.define("ruleactiontarget-replyto", MozRuleactiontargetReplyto); + customElements.define( + "ruleactiontarget-forwardto", + MozRuleactiontargetForwardto + ); + customElements.define("ruleactiontarget-folder", MozRuleactiontargetFolder); + customElements.define("ruleactiontarget-wrapper", MozRuleactiontargetWrapper); + + /** + * This is an abstract class for search menulist general functionality. + * + * @abstract + * @augments MozXULElement + */ + class MozSearchMenulistAbstract extends MozXULElement { + static get observedAttributes() { + return ["flex", "disabled"]; + } + + constructor() { + super(); + this.internalScope = null; + this.internalValue = -1; + this.validityManager = Cc[ + "@mozilla.org/mail/search/validityManager;1" + ].getService(Ci.nsIMsgSearchValidityManager); + } + + connectedCallback() { + if (this.delayConnectedCallback() || this.hasChildNodes()) { + return; + } + + this.menulist = document.createXULElement("menulist"); + this.menulist.classList.add("search-menulist"); + this.menulist.addEventListener("command", this.onSelect.bind(this)); + this.menupopup = document.createXULElement("menupopup"); + this.menupopup.classList.add("search-menulist-popup"); + this.menulist.appendChild(this.menupopup); + this.appendChild(this.menulist); + this._updateAttributes(); + } + + attributeChangedCallback() { + this._updateAttributes(); + } + + _updateAttributes() { + if (!this.menulist) { + return; + } + if (this.hasAttribute("flex")) { + this.menulist.setAttribute("flex", this.getAttribute("flex")); + } else { + this.menulist.removeAttribute("flex"); + } + if (this.hasAttribute("disabled")) { + this.menulist.setAttribute("disabled", this.getAttribute("disabled")); + } else { + this.menulist.removeAttribute("disabled"); + } + } + + set searchScope(val) { + // if scope isn't changing this is a noop + if (this.internalScope == val) { + return; + } + this.internalScope = val; + this.refreshList(); + if (this.targets) { + this.targets.forEach(target => { + customElements.upgrade(target); + target.searchScope = val; + }); + } + } + + get searchScope() { + return this.internalScope; + } + + get validityTable() { + return this.validityManager.getTable(this.searchScope); + } + + get targets() { + const forAttrs = this.getAttribute("for"); + if (!forAttrs) { + return null; + } + const targetIds = forAttrs.split(","); + if (targetIds.length == 0) { + return null; + } + + return targetIds + .map(id => document.getElementById(id)) + .filter(e => e != null); + } + + get optargets() { + const forAttrs = this.getAttribute("opfor"); + if (!forAttrs) { + return null; + } + const optargetIds = forAttrs.split(","); + if (optargetIds.length == 0) { + return null; + } + + return optargetIds + .map(id => document.getElementById(id)) + .filter(e => e != null); + } + + set value(val) { + if (this.internalValue == val) { + return; + } + this.internalValue = val; + this.menulist.selectedItem = this.validMenuitem; + // now notify targets of new parent's value + if (this.targets) { + this.targets.forEach(target => { + customElements.upgrade(target); + target.parentValue = val; + }); + } + // now notify optargets of new op parent's value + if (this.optargets) { + this.optargets.forEach(optarget => { + customElements.upgrade(optarget); + optarget.opParentValue = val; + }); + } + } + + get value() { + return this.internalValue; + } + + /** + * Gets the label of the menulist's selected item. + */ + get label() { + return this.menulist.selectedItem.getAttribute("label"); + } + + get validMenuitem() { + if (this.value == -1) { + // -1 means not initialized + return null; + } + let isCustom = isNaN(this.value); + let typedValue = isCustom ? this.value : parseInt(this.value); + // custom attribute to style the unavailable menulist item + this.menulist.setAttribute( + "unavailable", + !this.valueIds.includes(typedValue) ? "true" : null + ); + // add a hidden menulist item if value is missing + let menuitem = this.menulist.querySelector(`[value="${this.value}"]`); + if (!menuitem) { + // need to add a hidden menuitem + menuitem = this.menulist.appendItem(this.valueLabel, this.value); + menuitem.hidden = true; + } + return menuitem; + } + + refreshList(dontRestore) { + const menuItemIds = this.valueIds; + const menuItemStrings = this.valueStrings; + const popup = this.menupopup; + // save our old "value" so we can restore it later + let oldData; + if (!dontRestore) { + oldData = this.menulist.value; + } + // remove the old popup children + while (popup.hasChildNodes()) { + popup.lastChild.remove(); + } + let newSelection; + let customizePos = -1; + for (let i = 0; i < menuItemIds.length; i++) { + // create the menuitem + if (Ci.nsMsgSearchAttrib.OtherHeader == menuItemIds[i].toString()) { + customizePos = i; + } else { + const menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute("label", menuItemStrings[i]); + menuitem.setAttribute("value", menuItemIds[i]); + popup.appendChild(menuitem); + // try to restore the selection + if (!newSelection || oldData == menuItemIds[i].toString()) { + newSelection = menuitem; + } + } + } + if (customizePos != -1) { + const separator = document.createXULElement("menuseparator"); + popup.appendChild(separator); + const menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute("label", menuItemStrings[customizePos]); + menuitem.setAttribute("value", menuItemIds[customizePos]); + popup.appendChild(menuitem); + } + + // If we are either uninitialized, or if we are called because + // of a change in our parent, update the value to the + // default stored in newSelection. + if ((this.value == -1 || dontRestore) && newSelection) { + this.value = newSelection.getAttribute("value"); + } + this.menulist.selectedItem = this.validMenuitem; + } + + onSelect(event) { + if (this.menulist.value == Ci.nsMsgSearchAttrib.OtherHeader) { + // Customize menuitem selected. + let args = {}; + window.openDialog( + "chrome://messenger/content/CustomHeaders.xhtml", + "", + "modal,centerscreen,resizable,titlebar,chrome", + args + ); + // User may have removed the custom header currently selected + // in the menulist so temporarily set the selection to a safe value. + this.value = Ci.nsMsgSearchAttrib.OtherHeader; + // rebuild the menulist + UpdateAfterCustomHeaderChange(); + // Find the created or chosen custom header and select it. + let menuitem = null; + if (args.selectedVal) { + menuitem = this.menulist.querySelector( + `[label="${args.selectedVal}"]` + ); + } + if (menuitem) { + this.value = menuitem.value; + } else { + // Nothing was picked in the custom headers editor so just pick something + // instead of the current "Customize" menuitem. + this.value = this.menulist.getItemAtIndex(0).value; + } + } else { + this.value = this.menulist.value; + } + } + } + + /** + * The MozSearchAttribute widget is typically used in the search and filter dialogs to show a list + * of possible message headers. + * + * @augments MozSearchMenulistAbstract + */ + class MozSearchAttribute extends MozSearchMenulistAbstract { + constructor() { + super(); + + this.stringBundle = Services.strings.createBundle( + "chrome://messenger/locale/search-attributes.properties" + ); + } + + connectedCallback() { + super.connectedCallback(); + + initializeTermFromId(this.id); + } + + get valueLabel() { + if (isNaN(this.value)) { + // is this a custom term? + let customTerm = MailServices.filters.getCustomTerm(this.value); + if (customTerm) { + return customTerm.name; + } + // The custom term may be missing after the extension that added it + // was disabled or removed. We need to notify the user. + let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance( + Ci.nsIScriptError + ); + scriptError.init( + "Missing custom search term " + this.value, + null, + null, + 0, + 0, + Ci.nsIScriptError.errorFlag, + "component javascript" + ); + Services.console.logMessage(scriptError); + return this.stringBundle.GetStringFromName("MissingCustomTerm"); + } + return this.stringBundle.GetStringFromName( + this.validityManager.getAttributeProperty(parseInt(this.value)) + ); + } + + get valueIds() { + let result = this.validityTable.getAvailableAttributes(); + // add any available custom search terms + for (let customTerm of MailServices.filters.getCustomTerms()) { + // For custom terms, the array element is a string with the custom id + // instead of the integer attribute + if (customTerm.getAvailable(this.searchScope, null)) { + result.push(customTerm.id); + } + } + return result; + } + + get valueStrings() { + let strings = []; + let ids = this.valueIds; + let hdrsArray = null; + try { + let hdrs = Services.prefs.getCharPref("mailnews.customHeaders"); + hdrs = hdrs.replace(/\s+/g, ""); // remove white spaces before splitting + hdrsArray = hdrs.match(/[^:]+/g); + } catch (ex) {} + let j = 0; + for (let i = 0; i < ids.length; i++) { + if (isNaN(ids[i])) { + // Is this a custom search term? + let customTerm = MailServices.filters.getCustomTerm(ids[i]); + if (customTerm) { + strings[i] = customTerm.name; + } else { + strings[i] = ""; + } + } else if (ids[i] > Ci.nsMsgSearchAttrib.OtherHeader && hdrsArray) { + strings[i] = hdrsArray[j++]; + } else { + strings[i] = this.stringBundle.GetStringFromName( + this.validityManager.getAttributeProperty(ids[i]) + ); + } + } + return strings; + } + } + customElements.define("search-attribute", MozSearchAttribute); + + /** + * MozSearchOperator contains a list of operators that can be applied on search-attribute and + * search-value value. + * + * @augments MozSearchMenulistAbstract + */ + class MozSearchOperator extends MozSearchMenulistAbstract { + constructor() { + super(); + + this.stringBundle = Services.strings.createBundle( + "chrome://messenger/locale/search-operators.properties" + ); + } + + connectedCallback() { + super.connectedCallback(); + + this.searchAttribute = Ci.nsMsgSearchAttrib.Default; + } + + get valueLabel() { + return this.stringBundle.GetStringFromName(this.value); + } + + get valueIds() { + let isCustom = isNaN(this.searchAttribute); + if (isCustom) { + let customTerm = MailServices.filters.getCustomTerm( + this.searchAttribute + ); + if (customTerm) { + return customTerm.getAvailableOperators(this.searchScope); + } + return [Ci.nsMsgSearchOp.Contains]; + } + return this.validityTable.getAvailableOperators(this.searchAttribute); + } + + get valueStrings() { + let strings = []; + let ids = this.valueIds; + for (let i = 0; i < ids.length; i++) { + strings[i] = this.stringBundle.GetStringFromID(ids[i]); + } + return strings; + } + + set parentValue(val) { + if ( + this.searchAttribute == val && + val != Ci.nsMsgSearchAttrib.OtherHeader + ) { + return; + } + this.searchAttribute = val; + this.refreshList(true); // don't restore the selection, since searchvalue nulls it + if (val == Ci.nsMsgSearchAttrib.AgeInDays) { + // We want "Age in Days" to default to "is less than". + this.value = Ci.nsMsgSearchOp.IsLessThan; + } + } + + get parentValue() { + return this.searchAttribute; + } + } + customElements.define("search-operator", MozSearchOperator); + + /** + * MozSearchValue is a widget that allows selecting the value to search or filter on. It can be a + * text entry, priority, status, junk status, tags, hasAttachment status, and addressbook etc. + * + * @augments MozXULElement + */ + class MozSearchValue extends MozXULElement { + static get observedAttributes() { + return ["disabled"]; + } + + constructor() { + super(); + + this.addEventListener("keypress", event => { + if (event.keyCode != KeyEvent.DOM_VK_RETURN) { + return; + } + onEnterInSearchTerm(event); + }); + + this.internalOperator = null; + this.internalAttribute = null; + this.internalValue = null; + + this.inputType = "none"; + } + + connectedCallback() { + this.classList.add("input-container"); + } + + static get stringBundle() { + if (!this._stringBundle) { + this._stringBundle = Services.strings.createBundle( + "chrome://messenger/locale/messenger.properties" + ); + } + return this._stringBundle; + } + + /** + * Create a menulist to be used as the input. + * + * @param {object[]} itemDataList - An ordered list of items to add to the + * menulist. Each entry must have a 'value' property to be used as the + * item value. If the entry has a 'label' property, it will be used + * directly as the item label, otherwise it must identify a bundle string + * using the 'stringId' property. + * + * @returns {MozMenuList} - The newly created menulist. + */ + static _createMenulist(itemDataList) { + let menulist = document.createXULElement("menulist"); + menulist.classList.add("search-value-menulist"); + let menupopup = document.createXULElement("menupopup"); + menupopup.classList.add("search-value-popup"); + + let bundle = this.stringBundle; + + for (let itemData of itemDataList) { + let item = document.createXULElement("menuitem"); + item.classList.add("search-value-menuitem"); + item.label = + itemData.label || bundle.GetStringFromName(itemData.stringId); + item.value = itemData.value; + menupopup.appendChild(item); + } + menulist.appendChild(menupopup); + return menulist; + } + + /** + * Set the child input. The input will only be changed if the type changes. + * + * @param {string} type - The type of input to use. + * @param {string|number|undefined} value - A value to set on the input, or + * leave undefined to not change the value. See setInputValue. + */ + setInput(type, value) { + if (type != this.inputType) { + this.inputType = type; + this.input?.remove(); + let input; + switch (type) { + case "text": + input = document.createElement("input"); + input.classList.add("input-inline", "search-value-input"); + break; + case "date": + input = document.createElement("input"); + input.classList.add("input-inline", "search-value-input"); + if (!value) { + // Newly created date input shows today's date. + // value is expected in microseconds since epoch. + value = Date.now() * 1000; + } + break; + case "size": + input = document.createElement("input"); + input.type = "number"; + input.min = 0; + input.max = 1000000000; + input.classList.add("input-inline", "search-value-input"); + break; + case "age": + input = document.createElement("input"); + input.type = "number"; + input.min = -40000; // ~100 years. + input.max = 40000; + input.classList.add("input-inline", "search-value-input"); + break; + case "percent": + input = document.createElement("input"); + input.type = "number"; + input.min = 0; + input.max = 100; + input.classList.add("input-inline", "search-value-input"); + break; + case "priority": + input = this.constructor._createMenulist([ + { stringId: "priorityHighest", value: Ci.nsMsgPriority.highest }, + { stringId: "priorityHigh", value: Ci.nsMsgPriority.high }, + { stringId: "priorityNormal", value: Ci.nsMsgPriority.normal }, + { stringId: "priorityLow", value: Ci.nsMsgPriority.low }, + { stringId: "priorityLowest", value: Ci.nsMsgPriority.lowest }, + ]); + break; + case "status": + input = this.constructor._createMenulist([ + { stringId: "replied", value: Ci.nsMsgMessageFlags.Replied }, + { stringId: "read", value: Ci.nsMsgMessageFlags.Read }, + { stringId: "new", value: Ci.nsMsgMessageFlags.New }, + { stringId: "forwarded", value: Ci.nsMsgMessageFlags.Forwarded }, + { stringId: "flagged", value: Ci.nsMsgMessageFlags.Marked }, + ]); + break; + case "addressbook": + input = document.createXULElement("menulist", { + is: "menulist-addrbooks", + }); + input.setAttribute("localonly", "true"); + input.classList.add("search-value-menulist"); + if (!value) { + // Select the personal addressbook by default. + value = "jsaddrbook://abook.sqlite"; + } + break; + case "tags": + input = this.constructor._createMenulist( + MailServices.tags.getAllTags().map(taginfo => { + return { label: taginfo.tag, value: taginfo.key }; + }) + ); + break; + case "junk-status": + // "Junk Status is/isn't/is empty/isn't empty 'Junk'". + input = this.constructor._createMenulist([ + { stringId: "junk", value: Ci.nsIJunkMailPlugin.JUNK }, + ]); + break; + case "attachment-status": + // "Attachment Status is/isn't 'Has Attachments'". + input = this.constructor._createMenulist([ + { stringId: "hasAttachments", value: "0" }, + ]); + break; + case "junk-origin": + input = this.constructor._createMenulist([ + { stringId: "junkScoreOriginPlugin", value: "plugin" }, + { stringId: "junkScoreOriginUser", value: "user" }, + { stringId: "junkScoreOriginFilter", value: "filter" }, + { stringId: "junkScoreOriginWhitelist", value: "whitelist" }, + { stringId: "junkScoreOriginImapFlag", value: "imapflag" }, + ]); + break; + case "none": + input = null; + break; + case "custom": + // Used by extensions. + // FIXME: We need a better way for extensions to set a custom input. + input = document.createXULElement("hbox"); + input.setAttribute("flex", "1"); + input.classList.add("search-value-custom"); + break; + default: + throw new Error(`Unrecognised input type "${type}"`); + } + + this.input = input; + if (input) { + this.appendChild(input); + } + + this._updateAttributes(); + } + + this.setInputValue(value); + } + + /** + * Set the child input to the given value. + * + * @param {string|number} value - The value to set on the input. For "date" + * inputs, this should be a number of microseconds since the epoch. + */ + setInputValue(value) { + if (value === undefined) { + return; + } + switch (this.inputType) { + case "text": + case "size": + case "age": + case "percent": + this.input.value = value; + break; + case "date": + this.input.value = convertPRTimeToString(value); + break; + case "priority": + case "status": + case "addressbook": + case "tags": + case "junk-status": + case "attachment-status": + case "junk-origin": + let item = this.input.querySelector(`menuitem[value="${value}"]`); + if (item) { + this.input.selectedItem = item; + } + break; + case "none": + // Silently ignore the value. + break; + case "custom": + this.input.setAttribute("value", value); + break; + default: + throw new Error(`Unhandled input type "${this.inputType}"`); + } + } + + /** + * Get the child input's value. + * + * @returns {string|number} - The value set in the input. For "date" + * inputs, this is the number of microseconds since the epoch. + */ + getInputValue() { + switch (this.inputType) { + case "text": + case "size": + case "age": + case "percent": + return this.input.value; + case "date": + return convertStringToPRTime(this.input.value); + case "priority": + case "status": + case "addressbook": + case "tags": + case "junk-status": + case "attachment-status": + case "junk-origin": + return this.input.selectedItem.value; + case "none": + return ""; + case "custom": + return this.input.getAttribute("value"); + default: + throw new Error(`Unhandled input type "${this.inputType}"`); + } + } + + /** + * Get the element's displayed value. + * + * @returns {string} - The value seen by the user. + */ + getReadableValue() { + switch (this.inputType) { + case "text": + case "size": + case "age": + case "percent": + case "date": + return this.input.value; + case "priority": + case "status": + case "addressbook": + case "tags": + case "junk-status": + case "attachment-status": + case "junk-origin": + return this.input.selectedItem.label; + case "none": + return ""; + case "custom": + return this.input.getAttribute("value"); + default: + throw new Error(`Unhandled input type "${this.inputType}"`); + } + } + + attributeChangedCallback() { + this._updateAttributes(); + } + + _updateAttributes() { + if (!this.input) { + return; + } + if (this.hasAttribute("disabled")) { + this.input.setAttribute("disabled", this.getAttribute("disabled")); + } else { + this.input.removeAttribute("disabled"); + } + } + + /** + * Update the displayed input according to the selected sibling attributes + * and operators. + * + * @param {nsIMsgSearchValue} [value] - A value to display in the input. Or + * leave unset to not change the value. + */ + updateDisplay(value) { + let operator = Number(this.internalOperator); + switch (Number(this.internalAttribute)) { + // Use the index to hide/show the appropriate child. + case Ci.nsMsgSearchAttrib.Priority: + this.setInput("priority", value?.priority); + break; + case Ci.nsMsgSearchAttrib.MsgStatus: + this.setInput("status", value?.status); + break; + case Ci.nsMsgSearchAttrib.Date: + this.setInput("date", value?.date); + break; + case Ci.nsMsgSearchAttrib.Sender: + case Ci.nsMsgSearchAttrib.To: + case Ci.nsMsgSearchAttrib.ToOrCC: + case Ci.nsMsgSearchAttrib.AllAddresses: + case Ci.nsMsgSearchAttrib.CC: + if ( + operator == Ci.nsMsgSearchOp.IsntInAB || + operator == Ci.nsMsgSearchOp.IsInAB + ) { + this.setInput("addressbook", value?.str); + } else { + this.setInput("text", value?.str); + } + break; + case Ci.nsMsgSearchAttrib.Keywords: + this.setInput( + operator == Ci.nsMsgSearchOp.IsEmpty || + operator == Ci.nsMsgSearchOp.IsntEmpty + ? "none" + : "tags", + value?.str + ); + break; + case Ci.nsMsgSearchAttrib.JunkStatus: + this.setInput( + operator == Ci.nsMsgSearchOp.IsEmpty || + operator == Ci.nsMsgSearchOp.IsntEmpty + ? "none" + : "junk-status", + value?.junkStatus + ); + break; + case Ci.nsMsgSearchAttrib.HasAttachmentStatus: + this.setInput("attachment-status", value?.hasAttachmentStatus); + break; + case Ci.nsMsgSearchAttrib.JunkScoreOrigin: + this.setInput("junk-origin", value?.str); + break; + case Ci.nsMsgSearchAttrib.AgeInDays: + this.setInput("age", value?.age); + break; + case Ci.nsMsgSearchAttrib.Size: + this.setInput("size", value?.size); + break; + case Ci.nsMsgSearchAttrib.JunkPercent: + this.setInput("percent", value?.junkPercent); + break; + default: + if (isNaN(this.internalAttribute)) { + // Custom attribute, the internalAttribute is a string. + // FIXME: We need a better way for extensions to set a custom input. + this.setInput("custom", value?.str); + this.input.setAttribute("searchAttribute", this.internalAttribute); + } else { + this.setInput("text", value?.str); + } + break; + } + } + + /** + * The sibling operator type. + * + * @type {nsMsgSearchOpValue} + */ + set opParentValue(val) { + if (this.internalOperator == val) { + return; + } + this.internalOperator = val; + this.updateDisplay(); + } + + get opParentValue() { + return this.internalOperator; + } + + /** + * A duplicate of the searchAttribute property. + * + * @type {nsMsgSearchAttribValue} + */ + set parentValue(val) { + this.searchAttribute = val; + } + + get parentValue() { + return this.searchAttribute; + } + + /** + * The sibling attribute type. + * + * @type {nsMsgSearchAttribValue} + */ + set searchAttribute(val) { + if (this.internalAttribute == val) { + return; + } + this.internalAttribute = val; + this.updateDisplay(); + } + + get searchAttribute() { + return this.internalAttribute; + } + + /** + * The stored value for this element. + * + * Note that the input value is *derived* from this object when it is set. + * But changes to the input value using the UI will not change the stored + * value until the save method is called. + * + * @type {nsIMsgSearchValue} + */ + set value(val) { + // val is a nsIMsgSearchValue object + this.internalValue = val; + this.updateDisplay(val); + } + + get value() { + return this.internalValue; + } + + /** + * Updates the stored value for this element to reflect its current input + * value. + */ + save() { + let searchValue = this.value; + let searchAttribute = this.searchAttribute; + + searchValue.attrib = isNaN(searchAttribute) + ? Ci.nsMsgSearchAttrib.Custom + : searchAttribute; + switch (Number(searchAttribute)) { + case Ci.nsMsgSearchAttrib.Priority: + searchValue.priority = this.getInputValue(); + break; + case Ci.nsMsgSearchAttrib.MsgStatus: + searchValue.status = this.getInputValue(); + break; + case Ci.nsMsgSearchAttrib.AgeInDays: + searchValue.age = this.getInputValue(); + break; + case Ci.nsMsgSearchAttrib.Date: + searchValue.date = this.getInputValue(); + break; + case Ci.nsMsgSearchAttrib.JunkStatus: + searchValue.junkStatus = this.getInputValue(); + break; + case Ci.nsMsgSearchAttrib.HasAttachmentStatus: + searchValue.status = Ci.nsMsgMessageFlags.Attachment; + break; + case Ci.nsMsgSearchAttrib.JunkPercent: + searchValue.junkPercent = this.getInputValue(); + break; + case Ci.nsMsgSearchAttrib.Size: + searchValue.size = this.getInputValue(); + break; + default: + searchValue.str = this.getInputValue(); + break; + } + } + + /** + * Stores the displayed value for this element in the given object. + * + * Note that after this call, the stored value will remain pointing to the + * given searchValue object. + * + * @param {nsIMsgSearchValue} searchValue - The object to store the + * displayed value in. + */ + saveTo(searchValue) { + this.internalValue = searchValue; + this.save(); + } + } + customElements.define("search-value", MozSearchValue); + + // 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"); + } + { + /** + * The MozRuleactiontypeMenulist is a widget that allows selecting the actions from the given menulist for + * the selected folder. It gets displayed in the message filter dialog box. + * + * @augments {MozMenuList} + */ + class MozRuleactiontypeMenulist extends customElements.get("menulist") { + connectedCallback() { + super.connectedCallback(); + if (this.delayConnectedCallback() || this.hasConnected) { + return; + } + this.hasConnected = true; + + this.setAttribute("is", "ruleactiontype-menulist"); + this.addEventListener("command", event => { + this.parentNode.setAttribute("value", this.value); + checkActionsReorder(); + }); + + this.addEventListener("popupshowing", event => { + let unavailableActions = this.usedActionsList(); + for (let index = 0; index < this.menuitems.length; index++) { + let menu = this.menuitems[index]; + menu.setAttribute("disabled", menu.value in unavailableActions); + } + }); + + this.menuitems = this.getElementsByTagNameNS( + this.namespaceURI, + "menuitem" + ); + + // Force initialization of the menulist custom element first. + customElements.upgrade(this); + this.addCustomActions(); + this.hideInvalidActions(); + // Differentiate between creating a new, next available action, + // and creating a row which will be initialized with an action. + if (!this.parentNode.hasAttribute("initialActionIndex")) { + let unavailableActions = this.usedActionsList(); + // Select the first one that's not in the list. + for (let index = 0; index < this.menuitems.length; index++) { + let menu = this.menuitems[index]; + if (!(menu.value in unavailableActions) && !menu.hidden) { + this.value = menu.value; + this.parentNode.setAttribute("value", menu.value); + break; + } + } + } else { + this.parentNode.mActionTypeInitialized = true; + this.parentNode.clearInitialActionIndex(); + } + } + + hideInvalidActions() { + let menupopup = this.menupopup; + let scope = getScopeFromFilterList(gFilterList); + + // Walk through the list of filter actions and hide any actions which aren't valid + // for our given scope (news, imap, pop, etc) and context. + let elements; + + // Disable / enable all elements in the "filteractionlist" + // based on the scope and the "enablefornews" attribute. + elements = menupopup.getElementsByAttribute("enablefornews", "true"); + for (let i = 0; i < elements.length; i++) { + elements[i].hidden = scope != Ci.nsMsgSearchScope.newsFilter; + } + + elements = menupopup.getElementsByAttribute("enablefornews", "false"); + for (let i = 0; i < elements.length; i++) { + elements[i].hidden = scope == Ci.nsMsgSearchScope.newsFilter; + } + + elements = menupopup.getElementsByAttribute("enableforpop3", "true"); + for (let i = 0; i < elements.length; i++) { + elements[i].hidden = !( + gFilterList.folder.server.type == "pop3" || + gFilterList.folder.server.type == "none" + ); + } + + elements = menupopup.getElementsByAttribute("isCustom", "true"); + // Note there might be an additional element here as a placeholder + // for a missing action, so we iterate over the known actions + // instead of the elements. + for (let i = 0; i < gCustomActions.length; i++) { + elements[i].hidden = !gCustomActions[i].isValidForType( + gFilterType, + scope + ); + } + + // Disable "Reply with Template" if there are no templates. + if (this.findTemplates().length == 0) { + elements = menupopup.getElementsByAttribute( + "value", + "replytomessage" + ); + if (elements.length == 1) { + elements[0].hidden = true; + } + } + } + + addCustomActions() { + let menupopup = this.menupopup; + for (let i = 0; i < gCustomActions.length; i++) { + let customAction = gCustomActions[i]; + let menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute("label", customAction.name); + menuitem.setAttribute("value", customAction.id); + menuitem.setAttribute("isCustom", "true"); + menupopup.appendChild(menuitem); + } + } + + /** + * Returns a hash containing all of the filter actions which are currently + * being used by other filteractionrows. + * + * @returns {object} - a hash containing all of the filter actions which are + * currently being used by other filteractionrows. + */ + usedActionsList() { + let usedActions = {}; + let currentFilterActionRow = this.parentNode; + let listBox = currentFilterActionRow.parentNode; // need to account for the list item. + // Now iterate over each list item in the list box. + for (let index = 0; index < listBox.getRowCount(); index++) { + let filterActionRow = listBox.getItemAtIndex(index); + if (filterActionRow != currentFilterActionRow) { + let actionValue = filterActionRow.getAttribute("value"); + + // Let custom actions decide if dups are allowed. + let isCustom = false; + for (let i = 0; i < gCustomActions.length; i++) { + if (gCustomActions[i].id == actionValue) { + isCustom = true; + if (!gCustomActions[i].allowDuplicates) { + usedActions[actionValue] = true; + } + break; + } + } + + if (!isCustom) { + // The following actions can appear more than once in a single filter + // so do not set them as already used. + if ( + actionValue != "addtagtomessage" && + actionValue != "forwardmessage" && + actionValue != "copymessage" + ) { + usedActions[actionValue] = true; + } + // If either Delete message or Move message exists, disable the other one. + // It does not make sense to apply both to the same message. + if (actionValue == "deletemessage") { + usedActions.movemessage = true; + } else if (actionValue == "movemessage") { + usedActions.deletemessage = true; + } else if (actionValue == "markasread") { + // The same with Mark as read/Mark as Unread. + usedActions.markasunread = true; + } else if (actionValue == "markasunread") { + usedActions.markasread = true; + } + } + } + } + return usedActions; + } + + /** + * Check if there exist any templates in this account. + * + * @returns {object[]} - An array of template headers: each has a label and + * a value. + */ + findTemplates() { + let identities = MailServices.accounts.getIdentitiesForServer( + gFilterList.folder.server + ); + // Typically if this is Local Folders. + if (identities.length == 0) { + if (MailServices.accounts.defaultAccount) { + identities.push( + MailServices.accounts.defaultAccount.defaultIdentity + ); + } + } + + let templates = []; + let foldersScanned = []; + + for (let identity of identities) { + let enumerator = null; + let msgFolder = MailUtils.getExistingFolder( + identity.stationeryFolder + ); + // If we already processed this folder, do not set enumerator + // so that we skip this identity. + if (msgFolder && !foldersScanned.includes(msgFolder)) { + foldersScanned.push(msgFolder); + enumerator = msgFolder.msgDatabase.enumerateMessages(); + } + + if (!enumerator) { + continue; + } + + for (let header of enumerator) { + let uri = + msgFolder.URI + + "?messageId=" + + header.messageId + + "&subject=" + + header.mime2DecodedSubject; + templates.push({ label: header.mime2DecodedSubject, value: uri }); + } + } + return templates; + } + } + + customElements.define( + "ruleactiontype-menulist", + MozRuleactiontypeMenulist, + { extends: "menulist" } + ); + } + + /** + * The MozRuleactionRichlistitem is a widget which gives the options to filter + * the messages with following elements: ruleactiontype-menulist, ruleactiontarget-wrapper + * and button to add or remove the MozRuleactionRichlistitem. It gets added in the + * filterActionList richlistbox in the Filter Editor dialog. + * + * @augments {MozElements.MozRichlistitem} + */ + class MozRuleactionRichlistitem extends MozElements.MozRichlistitem { + static get inheritedAttributes() { + return { ".ruleactiontarget": "type=value" }; + } + + constructor() { + super(); + + this.mActionTypeInitialized = false; + this.mRuleActionTargetInitialized = false; + } + + connectedCallback() { + if (this.delayConnectedCallback() || this.hasChildNodes()) { + return; + } + this.setAttribute("is", "ruleaction-richlistitem"); + this.appendChild( + MozXULElement.parseXULToFragment( + ` + <menulist is="ruleactiontype-menulist" style="flex: &filterActionTypeFlexValue;"> + <menupopup> + <menuitem label="&moveMessage.label;" + value="movemessage" + enablefornews="false"></menuitem> + <menuitem label="©Message.label;" + value="copymessage"></menuitem> + <menuseparator enablefornews="false"></menuseparator> + <menuitem label="&forwardTo.label;" + value="forwardmessage" + enablefornews="false"></menuitem> + <menuitem label="&replyWithTemplate.label;" + value="replytomessage" + enablefornews="false"></menuitem> + <menuseparator></menuseparator> + <menuitem label="&markMessageRead.label;" + value="markasread"></menuitem> + <menuitem label="&markMessageUnread.label;" + value="markasunread"></menuitem> + <menuitem label="&markMessageStarred.label;" + value="markasflagged"></menuitem> + <menuitem label="&setPriority.label;" + value="setpriorityto"></menuitem> + <menuitem label="&addTag.label;" + value="addtagtomessage"></menuitem> + <menuitem label="&setJunkScore.label;" + value="setjunkscore" + enablefornews="false"></menuitem> + <menuseparator enableforpop3="true"></menuseparator> + <menuitem label="&deleteMessage.label;" + value="deletemessage"></menuitem> + <menuitem label="&deleteFromPOP.label;" + value="deletefrompopserver" + enableforpop3="true"></menuitem> + <menuitem label="&fetchFromPOP.label;" + value="fetchfrompopserver" + enableforpop3="true"></menuitem> + <menuseparator></menuseparator> + <menuitem label="&ignoreThread.label;" + value="ignorethread"></menuitem> + <menuitem label="&ignoreSubthread.label;" + value="ignoresubthread"></menuitem> + <menuitem label="&watchThread.label;" + value="watchthread"></menuitem> + <menuseparator></menuseparator> + <menuitem label="&stopExecution.label;" + value="stopexecution"></menuitem> + </menupopup> + </menulist> + <ruleactiontarget-wrapper class="ruleactiontarget" + style="flex: &filterActionTargetFlexValue;"> + </ruleactiontarget-wrapper> + <hbox> + <button class="small-button" + label="+" + tooltiptext="&addAction.tooltip;" + oncommand="this.parentNode.parentNode.addRow();"></button> + <button class="small-button remove-small-button" + label="−" + tooltiptext="&removeAction.tooltip;" + oncommand="this.parentNode.parentNode.removeRow();"></button> + </hbox> + `, + [ + "chrome://messenger/locale/messenger.dtd", + "chrome://messenger/locale/FilterEditor.dtd", + ] + ) + ); + + this.mRuleActionType = this.querySelector("menulist"); + this.mRemoveButton = this.querySelector(".remove-small-button"); + this.mListBox = this.parentNode; + this.initializeAttributeInheritance(); + } + + set selected(val) { + // This provides a dummy selected property that the richlistbox expects to + // be able to call. See bug 202036. + } + + get selected() { + return false; + } + + _fireEvent(aName) { + // This provides a dummy _fireEvent function that the richlistbox expects to + // be able to call. See bug 202036. + } + + /** + * We should only remove the initialActionIndex after we have been told that + * both the rule action type and the rule action target have both been built + * since they both need this piece of information. This complication arises + * because both of these child elements are getting bound asynchronously + * after the search row has been constructed. + */ + clearInitialActionIndex() { + if (this.mActionTypeInitialized && this.mRuleActionTargetInitialized) { + this.removeAttribute("initialActionIndex"); + } + } + + initWithAction(aFilterAction) { + let filterActionStr; + let actionTarget = this.children[1]; + let actionItem = actionTarget.ruleactiontargetElement; + let nsMsgFilterAction = Ci.nsMsgFilterAction; + switch (aFilterAction.type) { + case nsMsgFilterAction.Custom: + filterActionStr = aFilterAction.customId; + if (actionItem) { + actionItem.children[0].value = aFilterAction.strValue; + } + + // Make sure the custom action has been added. If not, it + // probably was from an extension that has been removed. We'll + // show a dummy menuitem to warn the user. + let needCustomLabel = true; + for (let i = 0; i < gCustomActions.length; i++) { + if (gCustomActions[i].id == filterActionStr) { + needCustomLabel = false; + break; + } + } + if (needCustomLabel) { + let menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute( + "label", + gFilterBundle.getString("filterMissingCustomAction") + ); + menuitem.setAttribute("value", filterActionStr); + menuitem.disabled = true; + this.mRuleActionType.menupopup.appendChild(menuitem); + let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance( + Ci.nsIScriptError + ); + scriptError.init( + "Missing custom action " + filterActionStr, + null, + null, + 0, + 0, + Ci.nsIScriptError.errorFlag, + "component javascript" + ); + Services.console.logMessage(scriptError); + } + break; + case nsMsgFilterAction.MoveToFolder: + case nsMsgFilterAction.CopyToFolder: + actionItem.children[0].value = aFilterAction.targetFolderUri; + break; + case nsMsgFilterAction.Reply: + case nsMsgFilterAction.Forward: + actionItem.children[0].value = aFilterAction.strValue; + break; + case nsMsgFilterAction.ChangePriority: + actionItem.children[0].value = aFilterAction.priority; + break; + case nsMsgFilterAction.JunkScore: + actionItem.children[0].value = aFilterAction.junkScore; + break; + case nsMsgFilterAction.AddTag: + actionItem.children[0].value = aFilterAction.strValue; + break; + default: + break; + } + if (aFilterAction.type != nsMsgFilterAction.Custom) { + filterActionStr = gFilterActionStrings[aFilterAction.type]; + } + this.mRuleActionType.value = filterActionStr; + this.mRuleActionTargetInitialized = true; + this.clearInitialActionIndex(); + checkActionsReorder(); + } + + /** + * Function is used to check if the filter is valid or not. This routine + * also prompts the user. + * + * @returns {boolean} - true if this row represents a valid filter action. + */ + validateAction() { + let filterActionString = this.getAttribute("value"); + let actionTarget = this.children[1]; + let actionTargetLabel = + actionTarget.ruleactiontargetElement && + actionTarget.ruleactiontargetElement.children[0].value; + let errorString, customError; + + switch (filterActionString) { + case "movemessage": + case "copymessage": + let msgFolder = actionTargetLabel + ? MailUtils.getOrCreateFolder(actionTargetLabel) + : null; + if (!msgFolder || !msgFolder.canFileMessages) { + errorString = "mustSelectFolder"; + } + break; + case "forwardmessage": + if ( + actionTargetLabel.length < 3 || + actionTargetLabel.indexOf("@") < 1 + ) { + errorString = "enterValidEmailAddress"; + } + break; + case "replytomessage": + if (!actionTarget.ruleactiontargetElement.children[0].selectedItem) { + errorString = "pickTemplateToReplyWith"; + } + break; + default: + // Locate the correct custom action, and check validity. + for (let i = 0; i < gCustomActions.length; i++) { + if (gCustomActions[i].id == filterActionString) { + customError = gCustomActions[i].validateActionValue( + actionTargetLabel, + gFilterList.folder, + gFilterType + ); + break; + } + } + break; + } + + errorString = errorString + ? gFilterBundle.getString(errorString) + : customError; + if (errorString) { + Services.prompt.alert(window, null, errorString); + } + + return !errorString; + } + + /** + * Create a new filter action, fill it in, and then append it to the filter. + * + * @param {object} aFilter - filter object to save. + */ + saveToFilter(aFilter) { + let filterAction = aFilter.createAction(); + let filterActionString = this.getAttribute("value"); + filterAction.type = gFilterActionStrings.indexOf(filterActionString); + let actionTarget = this.children[1]; + let actionItem = actionTarget.ruleactiontargetElement; + let nsMsgFilterAction = Ci.nsMsgFilterAction; + switch (filterAction.type) { + case nsMsgFilterAction.ChangePriority: + filterAction.priority = actionItem.children[0].getAttribute("value"); + break; + case nsMsgFilterAction.MoveToFolder: + case nsMsgFilterAction.CopyToFolder: + filterAction.targetFolderUri = actionItem.children[0].value; + break; + case nsMsgFilterAction.JunkScore: + filterAction.junkScore = actionItem.children[0].value; + break; + case nsMsgFilterAction.Custom: + filterAction.customId = filterActionString; + // Fall through to set the value. + default: + if (actionItem && actionItem.children.length > 0) { + filterAction.strValue = actionItem.children[0].value; + } + break; + } + aFilter.appendAction(filterAction); + } + + /** + * If we only have one row of actions, then disable the remove button for that row. + */ + updateRemoveButton() { + this.mListBox.getItemAtIndex(0).mRemoveButton.disabled = + this.mListBox.getRowCount() == 1; + } + + addRow() { + let listItem = document.createXULElement("richlistitem", { + is: "ruleaction-richlistitem", + }); + listItem.classList.add("ruleaction"); + listItem.setAttribute("onfocus", "this.storeFocus();"); + this.mListBox.insertBefore(listItem, this.nextElementSibling); + this.mListBox.ensureElementIsVisible(listItem); + + // Make sure the first remove button is enabled. + this.updateRemoveButton(); + checkActionsReorder(); + } + + removeRow() { + // this.mListBox will fail after the row is removed, so save a reference. + let listBox = this.mListBox; + if (listBox.getRowCount() > 1) { + this.remove(); + } + // Can't use 'this' as it is destroyed now. + listBox.getItemAtIndex(0).updateRemoveButton(); + checkActionsReorder(); + } + + /** + * When this action row is focused, store its index in the parent richlistbox. + */ + storeFocus() { + this.mListBox.setAttribute( + "focusedAction", + this.mListBox.getIndexOfItem(this) + ); + } + } + + customElements.define("ruleaction-richlistitem", MozRuleactionRichlistitem, { + extends: "richlistitem", + }); +} |