/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * 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/. */ var { PluralForm } = ChromeUtils.importESModule( "resource://gre/modules/PluralForm.sys.mjs" ); var { MailServices } = ChromeUtils.import( "resource:///modules/MailServices.jsm" ); window.addEventListener("load", onLoad); window.addEventListener("unload", onFilterUnload); window.addEventListener("close", event => { if (!onFilterClose()) { event.preventDefault(); } }); var gFilterListMsgWindow = null; var gCurrentFilterList; var gServerMenu = null; var gFilterListbox = null; var gEditButton = null; var gDeleteButton = null; var gCopyToNewButton = null; var gTopButton = null; var gUpButton = null; var gDownButton = null; var gBottomButton = null; var gSearchBox = null; var gRunFiltersFolder = null; var gRunFiltersButton = null; var gFilterBundle = null; var msgMoveMotion = { Up: 0, Down: 1, Top: 2, Bottom: 3, }; var gStatusFeedback = { progressMeterVisible: false, showStatusString(status) { document.getElementById("statusText").setAttribute("value", status); }, startMeteors() { // change run button to be a stop button gRunFiltersButton.setAttribute( "label", gRunFiltersButton.getAttribute("stoplabel") ); gRunFiltersButton.setAttribute( "accesskey", gRunFiltersButton.getAttribute("stopaccesskey") ); if (!this.progressMeterVisible) { document .getElementById("statusbar-progresspanel") .removeAttribute("collapsed"); this.progressMeterVisible = true; } document.getElementById("statusbar-icon").removeAttribute("value"); }, stopMeteors() { try { // change run button to be a stop button gRunFiltersButton.setAttribute( "label", gRunFiltersButton.getAttribute("runlabel") ); gRunFiltersButton.setAttribute( "accesskey", gRunFiltersButton.getAttribute("runaccesskey") ); if (this.progressMeterVisible) { document.getElementById("statusbar-progresspanel").collapsed = true; this.progressMeterVisible = true; } } catch (ex) { // can get here if closing window when running filters } }, showProgress(percentage) {}, closeWindow() {}, }; var filterEditorQuitObserver = { observe(aSubject, aTopic, aData) { // Check whether or not we want to veto the quit request (unless another // observer already did. if ( aTopic == "quit-application-requested" && aSubject instanceof Ci.nsISupportsPRBool && !aSubject.data ) { aSubject.data = !onFilterClose(); } }, }; function onLoad() { gFilterListMsgWindow = Cc[ "@mozilla.org/messenger/msgwindow;1" ].createInstance(Ci.nsIMsgWindow); gFilterListMsgWindow.domWindow = window; gFilterListMsgWindow.rootDocShell.appType = Ci.nsIDocShell.APP_TYPE_MAIL; gFilterListMsgWindow.statusFeedback = gStatusFeedback; gServerMenu = document.getElementById("serverMenu"); gFilterListbox = document.getElementById("filterList"); gEditButton = document.getElementById("editButton"); gDeleteButton = document.getElementById("deleteButton"); gCopyToNewButton = document.getElementById("copyToNewButton"); gTopButton = document.getElementById("reorderTopButton"); gUpButton = document.getElementById("reorderUpButton"); gDownButton = document.getElementById("reorderDownButton"); gBottomButton = document.getElementById("reorderBottomButton"); gSearchBox = document.getElementById("searchBox"); gRunFiltersFolder = document.getElementById("runFiltersFolder"); gRunFiltersButton = document.getElementById("runFiltersButton"); gFilterBundle = document.getElementById("bundle_filter"); updateButtons(); initNewToolbarButtons(document.querySelector("#newButton toolbarbutton")); initNewToolbarButtons(document.querySelector("#newButton dropmarker")); document .getElementById("filterActionButtons") .addEventListener("keypress", event => onFilterActionButtonKeyPress(event)); processWindowArguments(window.arguments[0]); // Don't change width after initial layout, so buttons stay within the dialog. gRunFiltersFolder.style.maxWidth = gRunFiltersFolder.getBoundingClientRect().width + "px"; Services.obs.addObserver( filterEditorQuitObserver, "quit-application-requested" ); } /** * Set up the toolbarbutton to have an index and an EvenListener for proper * keyboard navigation. * * @param {XULElement} newToolbarbutton - The toolbarbutton that needs to be * initialized. */ function initNewToolbarButtons(newToolbarbutton) { newToolbarbutton.setAttribute("tabindex", "0"); newToolbarbutton.setAttribute( "id", newToolbarbutton.parentNode.id + newToolbarbutton.tagName ); } /** * Processes arguments sent to this dialog when opened or refreshed. * * @param aArguments An object having members representing the arguments. * { arg1: value1, arg2: value2, ... } */ function processWindowArguments(aArguments) { // If a specific folder was requested, try to select it // if we don't already show its server. if ( !gServerMenu._folder || ("folder" in aArguments && aArguments.folder != gServerMenu._folder && aArguments.folder.rootFolder != gServerMenu._folder) ) { let wantedFolder; if ("folder" in aArguments) { wantedFolder = aArguments.folder; } // Get the folder where filters should be defined, if that server // can accept filters. let firstItem = getFilterFolderForSelection(wantedFolder); // If the selected server cannot have filters, get the default server // If the default server cannot have filters, check all accounts // and get a server that can have filters. if (!firstItem) { firstItem = getServerThatCanHaveFilters().rootFolder; } if (firstItem) { setFilterFolder(firstItem); } if (wantedFolder) { setRunFolder(wantedFolder); } } else { // If we didn't change folder still redraw the list // to show potential new filters if we were called for refresh. rebuildFilterList(); } // If a specific filter was requested, try to select it. if ("filter" in aArguments) { selectFilter(aArguments.filter); } } /** * This is called from OpenOrFocusWindow() if the dialog is already open. * New filters could have been created by operations outside the dialog. * * @param aArguments An object of arguments having the same format * as window.arguments[0]. */ function refresh(aArguments) { // As we really don't know what has changed, clear the search box // undonditionally so that the changed/added filters are surely visible. resetSearchBox(); processWindowArguments(aArguments); } function CanRunFiltersAfterTheFact(aServer) { // filter after the fact is implement using search // so if you can't search, you can't filter after the fact return aServer.canSearchMessages; } /** * Change the root server for which we are managing filters. * * @param msgFolder The nsIMsgFolder server containing filters * (or a folder for NNTP server). */ function setFilterFolder(msgFolder) { if (!msgFolder || msgFolder == gServerMenu._folder) { return; } // Save the current filters to disk before switching because // the dialog may be closed and we'll lose current filters. if (gCurrentFilterList) { gCurrentFilterList.saveToDefaultFile(); } // Setting this attribute should go away in bug 473009. gServerMenu._folder = msgFolder; // Calling this should go away in bug 802609. gServerMenu.menupopup.selectFolder(msgFolder); // Calling getEditableFilterList will detect any errors in msgFilterRules.dat, // backup the file, and alert the user. gCurrentFilterList = msgFolder.getEditableFilterList(gFilterListMsgWindow); rebuildFilterList(); // Select the first item in the list, if there is one. if (gFilterListbox.itemCount > 0) { gFilterListbox.selectItem(gFilterListbox.getItemAtIndex(0)); } // This will get the deferred to account root folder, if server is deferred. // We intentionally do this after setting the current server, as we want // that to refer to the rootFolder for the actual server, not the // deferred-to server, as current server is really a proxy for the // server whose filters we are editing. But below here we are managing // where the filters will get applied, which is on the deferred-to server. msgFolder = msgFolder.server.rootMsgFolder; // root the folder picker to this server let runMenu = gRunFiltersFolder.menupopup; runMenu._teardown(); runMenu._parentFolder = msgFolder; runMenu._ensureInitialized(); let canFilterAfterTheFact = CanRunFiltersAfterTheFact(msgFolder.server); gRunFiltersFolder.disabled = !canFilterAfterTheFact; gRunFiltersButton.disabled = !canFilterAfterTheFact; document.getElementById("folderPickerPrefix").disabled = !canFilterAfterTheFact; if (canFilterAfterTheFact) { let wantedFolder = null; // For a given server folder, get the default run target folder or show // "Choose Folder". if (!msgFolder.isServer) { wantedFolder = msgFolder; } else { try { switch (msgFolder.server.type) { case "nntp": // For NNTP select the subscribed newsgroup. wantedFolder = gServerMenu._folder; break; case "rss": // Show "Choose Folder" for feeds. wantedFolder = null; break; case "imap": case "pop3": case "none": // Find Inbox for IMAP and POP or Local Folders, // show "Choose Folder" if not found. wantedFolder = msgFolder.rootFolder.getFolderWithFlags( Ci.nsMsgFolderFlags.Inbox ); break; default: // For other account types we don't know what's good to select, // so show "Choose Folder". wantedFolder = null; } } catch (e) { console.error( "Failed to select a suitable folder to run filters on: " + e ); wantedFolder = null; } } // Select a useful first folder for the server. setRunFolder(wantedFolder); } } /** * Select a folder on which filters are to be run. * * @param aFolder nsIMsgFolder folder to select. */ function setRunFolder(aFolder) { // Setting this attribute should go away in bug 473009. gRunFiltersFolder._folder = aFolder; // Calling this should go away in bug 802609. gRunFiltersFolder.menupopup.selectFolder(gRunFiltersFolder._folder); updateButtons(); } /** * Toggle enabled state of a filter, in both the filter properties and the UI. * * @param aFilterItem an item (row) of the filter list to be toggled */ function toggleFilter(aFilterItem, aSetForEvent) { let filter = aFilterItem._filter; if (filter.unparseable && !filter.enabled) { Services.prompt.alert( window, null, gFilterBundle.getFormattedString("cannotEnableIncompatFilter", [ document.getElementById("bundle_brand").getString("brandShortName"), ]) ); return; } filter.enabled = aSetForEvent === undefined ? !filter.enabled : aSetForEvent; // Now update the checkbox if (aSetForEvent === undefined) { aFilterItem.firstElementChild.nextElementSibling.checked = filter.enabled; } // For accessibility set the checked state on listitem aFilterItem.setAttribute("aria-checked", filter.enabled); } /** * Selects a specific filter in the filter list. * The listbox view is scrolled to the corresponding item. * * @param aFilter The nsIMsgFilter to select. * * @returns true/false indicating whether the filter was found and selected. */ function selectFilter(aFilter) { if (currentFilter() == aFilter) { return true; } resetSearchBox(aFilter); let filterCount = gCurrentFilterList.filterCount; for (let i = 0; i < filterCount; i++) { if (gCurrentFilterList.getFilterAt(i) == aFilter) { gFilterListbox.ensureIndexIsVisible(i); gFilterListbox.selectedIndex = i; return true; } } return false; } /** * Returns the currently selected filter. If multiple filters are selected, * returns the first one. If none are selected, returns null. */ function currentFilter() { let currentItem = gFilterListbox.selectedItem; return currentItem ? currentItem._filter : null; } function onEditFilter() { if (gEditButton.disabled) { return; } let selectedFilter = currentFilter(); if (!selectedFilter) { return; } let args = { filter: selectedFilter, filterList: gCurrentFilterList }; window.openDialog( "chrome://messenger/content/FilterEditor.xhtml", "FilterEditor", "chrome,modal,titlebar,resizable,centerscreen", args ); if ("refresh" in args && args.refresh) { // reset search if edit was okay (name change might lead to hidden entry!) resetSearchBox(selectedFilter); rebuildFilterList(); } } /** * Handler function for the 'New...' buttons. * Opens the filter dialog for creating a new filter. */ function onNewFilter() { calculatePositionAndShowCreateFilterDialog({}); } /** * Handler function for the 'Copy...' button. * Opens the filter dialog for copying the selected filter. */ function onCopyToNewFilter() { if (gCopyToNewButton.disabled) { return; } let selectedFilter = currentFilter(); if (!selectedFilter) { return; } let args = { copiedFilter: selectedFilter }; calculatePositionAndShowCreateFilterDialog(args); } /** * Calculates the position for inserting the new filter, * and then displays the create dialog. * * @param args The object containing the arguments for the dialog, * passed to the filterEditorOnLoad() function. * It will be augmented with the insertion position * and global filters list properties by this function. */ function calculatePositionAndShowCreateFilterDialog(args) { let selectedFilter = currentFilter(); // If no filter is selected use the first position. let position = 0; if (selectedFilter) { // Get the position in the unfiltered list. // - this is where the new filter should be inserted! let filterCount = gCurrentFilterList.filterCount; for (let i = 0; i < filterCount; i++) { if (gCurrentFilterList.getFilterAt(i) == selectedFilter) { position = i; break; } } } args.filterPosition = position; args.filterList = gCurrentFilterList; window.openDialog( "chrome://messenger/content/FilterEditor.xhtml", "FilterEditor", "chrome,modal,titlebar,resizable,centerscreen", args ); if ("refresh" in args && args.refresh) { // On success: reset the search box if necessary! resetSearchBox(args.newFilter); rebuildFilterList(); // Select the new filter, it is at the position of previous selection. gFilterListbox.selectItem(gFilterListbox.getItemAtIndex(position)); if (currentFilter() != args.newFilter) { console.error("Filter created at an unexpected position!"); } } } /** * Delete selected filters. * 'Selected' is not to be confused with active (checkbox checked) */ function onDeleteFilter() { if (gDeleteButton.disabled) { return; } let items = gFilterListbox.selectedItems; if (!items.length) { return; } let checkValue = { value: false }; if ( Services.prefs.getBoolPref("mailnews.filters.confirm_delete") && Services.prompt.confirmEx( window, null, gFilterBundle.getString("deleteFilterConfirmation"), Services.prompt.STD_YES_NO_BUTTONS, "", "", "", gFilterBundle.getString("dontWarnAboutDeleteCheckbox"), checkValue ) ) { return; } if (checkValue.value) { Services.prefs.setBoolPref("mailnews.filters.confirm_delete", false); } // Save filter position before the first selected one. let newSelectionIndex = gFilterListbox.selectedIndex - 1; // Must reverse the loop, as the items list shrinks when we delete. for (let index = items.length - 1; index >= 0; --index) { let item = items[index]; gCurrentFilterList.removeFilter(item._filter); item.remove(); } updateCountBox(); // Select filter above previously selected if one existed, otherwise the first one. if (newSelectionIndex == -1 && gFilterListbox.itemCount > 0) { newSelectionIndex = 0; } if (newSelectionIndex > -1) { gFilterListbox.selectedIndex = newSelectionIndex; updateViewPosition(-1); } } /** * Move filter one step up in visible list. */ function onUp(event) { moveFilter(msgMoveMotion.Up); } /** * Move filter one step down in visible list. */ function onDown(event) { moveFilter(msgMoveMotion.Down); } /** * Move filter to bottom for long filter lists. */ function onTop(evt) { moveFilter(msgMoveMotion.Top); } /** * Move filter to top for long filter lists. */ function onBottom(evt) { moveFilter(msgMoveMotion.Bottom); } /** * Moves a singular selected filter up or down either 1 increment or to the * top/bottom. This acts on the visible filter list only which means that: * * - when moving up or down "1" the filter may skip one or more other * filters (which are currently not visible) - this will also lead * to the "related" filters (e.g search filters containing 'moz') * being grouped more closely together * - moveTop / moveBottom * this is currently moving to the top/bottom of the absolute list * but it would be better if it moved "just as far as necessary" * which would further "compact" related filters * * @param motion * msgMoveMotion.Up, msgMoveMotion.Down, msgMoveMotion.Top, msgMoveMotion.Bottom */ function moveFilter(motion) { // At the moment, do not allow moving groups of filters. let selectedFilter = currentFilter(); if (!selectedFilter) { return; } var relativeStep = 0; var moveFilterNative = null; switch (motion) { case msgMoveMotion.Top: if (selectedFilter) { gCurrentFilterList.removeFilter(selectedFilter); gCurrentFilterList.insertFilterAt(0, selectedFilter); rebuildFilterList(); } return; case msgMoveMotion.Bottom: if (selectedFilter) { gCurrentFilterList.removeFilter(selectedFilter); gCurrentFilterList.insertFilterAt( gCurrentFilterList.filterCount, selectedFilter ); rebuildFilterList(); } return; case msgMoveMotion.Up: relativeStep = -1; moveFilterNative = Ci.nsMsgFilterMotion.up; break; case msgMoveMotion.Down: relativeStep = +1; moveFilterNative = Ci.nsMsgFilterMotion.down; break; } if (!gSearchBox.value) { // use legacy move filter code: up, down; only if searchBox is empty moveCurrentFilter(moveFilterNative); return; } let nextIndex = gFilterListbox.selectedIndex + relativeStep; let nextFilter = gFilterListbox.getItemAtIndex(nextIndex)._filter; gCurrentFilterList.removeFilter(selectedFilter); // Find the index of the filter we want to insert at. let newIndex = -1; let filterCount = gCurrentFilterList.filterCount; for (let i = 0; i < filterCount; i++) { if (gCurrentFilterList.getFilterAt(i) == nextFilter) { newIndex = i; break; } } if (motion == msgMoveMotion.Down) { newIndex += relativeStep; } gCurrentFilterList.insertFilterAt(newIndex, selectedFilter); rebuildFilterList(); } function viewLog() { var args = { filterList: gCurrentFilterList }; window.openDialog( "chrome://messenger/content/viewLog.xhtml", "FilterLog", "chrome,modal,titlebar,resizable,centerscreen", args ); } function onFilterUnload() { gCurrentFilterList.saveToDefaultFile(); Services.obs.removeObserver( filterEditorQuitObserver, "quit-application-requested" ); gFilterListMsgWindow.closeWindow(); } function onFilterClose() { if ( gRunFiltersButton.getAttribute("label") == gRunFiltersButton.getAttribute("stoplabel") ) { let promptTitle = gFilterBundle.getString("promptTitle"); let promptMsg = gFilterBundle.getString("promptMsg"); let stopButtonLabel = gFilterBundle.getString("stopButtonLabel"); let continueButtonLabel = gFilterBundle.getString("continueButtonLabel"); let result = Services.prompt.confirmEx( window, promptTitle, promptMsg, Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 + Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1, continueButtonLabel, stopButtonLabel, null, null, { value: 0 } ); if (result) { gFilterListMsgWindow.StopUrls(); } else { return false; } } return true; } function runSelectedFilters() { // if run button has "stop" label, do stop. if ( gRunFiltersButton.getAttribute("label") == gRunFiltersButton.getAttribute("stoplabel") ) { gFilterListMsgWindow.StopUrls(); return; } let folder = gRunFiltersFolder._folder || gRunFiltersFolder.selectedItem._folder; if (!folder) { return; } let filterList = MailServices.filters.getTempFilterList(folder); // make sure the tmp filter list uses the real filter list log stream filterList.loggingEnabled = gCurrentFilterList.loggingEnabled; filterList.logStream = gCurrentFilterList.logStream; let index = 0; for (let item of gFilterListbox.selectedItems) { filterList.insertFilterAt(index++, item._filter); } MailServices.filters.applyFiltersToFolders( filterList, [folder], gFilterListMsgWindow ); } function moveCurrentFilter(motion) { let filter = currentFilter(); if (!filter) { return; } gCurrentFilterList.moveFilter(filter, motion); rebuildFilterList(); } /** * Redraws the list of filters. Takes the search box value into account. * * This function should perform very fast even in case of high number of filters. * Therefore there are some optimizations (e.g. listelement.itemChildren[] instead of * list.getItemAtIndex()), that favour speed vs. semantical perfection. */ function rebuildFilterList() { // Get filters that match the search box. let aTempFilterList = onFindFilter(); let searchBoxFocus = false; let activeElement = document.activeElement; // Find if the currently focused element is a child inside the search box // (probably html:input). Traverse up the parents until the first element // with an ID is found. If it is not searchBox, return false. while (activeElement != null) { if (activeElement == gSearchBox) { searchBoxFocus = true; break; } else if (activeElement.id) { searchBoxFocus = false; break; } activeElement = activeElement.parentNode; } // Make a note of which filters were previously selected let selectedNames = []; for (let i = 0; i < gFilterListbox.selectedItems.length; i++) { selectedNames.push(gFilterListbox.selectedItems[i]._filter.filterName); } // Save scroll position so we can try to restore it later. // Doesn't work when the list is rebuilt after search box condition changed. let firstVisibleRowIndex = gFilterListbox.getIndexOfFirstVisibleRow(); // listbox.xml seems to cache the value of the first selected item in a // range at _selectionStart. The old value though is now obsolete, // since we will recreate all of the elements. We need to clear this, // and one way to do this is with a call to clearSelection. This might be // ugly from an accessibility perspective, since it fires an onSelect event. gFilterListbox.clearSelection(); let listitem, nameCell, enabledCell, filter; let filterCount = gCurrentFilterList.filterCount; let listitemCount = gFilterListbox.itemCount; let listitemIndex = 0; let tempFilterListLength = aTempFilterList ? aTempFilterList.length - 1 : 0; for (let i = 0; i < filterCount; i++) { if (aTempFilterList && listitemIndex > tempFilterListLength) { break; } filter = gCurrentFilterList.getFilterAt(i); if (aTempFilterList && aTempFilterList[listitemIndex] != i) { continue; } if (listitemCount > listitemIndex) { // If there is a free existing listitem, reuse it. // Use .itemChildren[] instead of .getItemAtIndex() as it is much faster. listitem = gFilterListbox.itemChildren[listitemIndex]; nameCell = listitem.firstElementChild; enabledCell = nameCell.nextElementSibling; } else { // If there are not enough listitems in the list, create a new one. listitem = document.createXULElement("richlistitem"); listitem.setAttribute("align", "center"); listitem.setAttribute("role", "checkbox"); nameCell = document.createXULElement("label"); nameCell.setAttribute("flex", "1"); nameCell.setAttribute("crop", "end"); enabledCell = document.createXULElement("checkbox"); enabledCell.setAttribute("style", "padding-inline-start: 25px;"); enabledCell.addEventListener("CheckboxStateChange", onFilterClick, true); listitem.appendChild(nameCell); listitem.appendChild(enabledCell); gFilterListbox.appendChild(listitem); // We have to attach this listener to the listitem, even though we only care // about clicks on the enabledCell. However, attaching to that item doesn't // result in any events actually getting received. listitem.addEventListener("dblclick", onFilterDoubleClick, true); } // For accessibility set the label on listitem. listitem.setAttribute("label", filter.filterName); // Set the listitem values to represent the current filter. nameCell.setAttribute("value", filter.filterName); if (filter.enabled) { enabledCell.setAttribute("checked", "true"); } else { enabledCell.removeAttribute("checked"); } listitem.setAttribute("aria-checked", filter.enabled); listitem._filter = filter; if (selectedNames.includes(filter.filterName)) { gFilterListbox.addItemToSelection(listitem); } listitemIndex++; } // Remove any superfluous listitems, if the number of filters shrunk. for (let i = listitemCount - 1; i >= listitemIndex; i--) { gFilterListbox.lastChild.remove(); } updateViewPosition(firstVisibleRowIndex); updateCountBox(); // If before rebuilding the list the searchbox was focused, focus it again. // In any other case, focus the list. if (searchBoxFocus) { gSearchBox.focus(); } else { gFilterListbox.focus(); } } function updateViewPosition(firstVisibleRowIndex) { if (firstVisibleRowIndex == -1) { firstVisibleRowIndex = gFilterListbox.getIndexOfFirstVisibleRow(); } // Restore to the extent possible the scroll position. if (firstVisibleRowIndex && gFilterListbox.itemCount) { gFilterListbox.ensureElementIsVisible( gFilterListbox.getItemAtIndex( Math.min(firstVisibleRowIndex, gFilterListbox.itemCount - 1) ), true ); } if (gFilterListbox.selectedCount) { // Make sure that at least the first selected item is visible. gFilterListbox.ensureElementIsVisible(gFilterListbox.selectedItems[0]); // The current item should be the first selected item, so that keyboard // selection extension can work. gFilterListbox.currentItem = gFilterListbox.selectedItems[0]; } updateButtons(); } /** * Try to only enable buttons that make sense * - moving filters is currently only enabled for single selection * also movement is restricted by searchBox and current selection position * - edit only for single filters * - delete / run only for one or more selected filters */ function updateButtons() { var numFiltersSelected = gFilterListbox.selectedItems.length; var oneFilterSelected = numFiltersSelected == 1; // "edit" is disabled when not exactly one filter is selected // or if we couldn't parse that filter let disabled = !oneFilterSelected || currentFilter().unparseable; gEditButton.disabled = disabled; // "copy" is the same as "edit" gCopyToNewButton.disabled = disabled; // "delete" only disabled when no filters are selected gDeleteButton.disabled = !numFiltersSelected; // we can run multiple filters on a folder // so only disable this UI if no filters are selected document.getElementById("folderPickerPrefix").disabled = !numFiltersSelected; gRunFiltersFolder.disabled = !numFiltersSelected; gRunFiltersButton.disabled = !numFiltersSelected || !gRunFiltersFolder._folder; // "up" and "top" enabled only if one filter is selected, and it's not the first // don't use gFilterListbox.currentIndex here, it's buggy when we've just changed the // children in the list (via rebuildFilterList) disabled = !( oneFilterSelected && gFilterListbox.getSelectedItem(0) != gFilterListbox.getItemAtIndex(0) ); gUpButton.disabled = disabled; gTopButton.disabled = disabled; // "down" and "bottom" enabled only if one filter is selected, // and it's not the last one disabled = !( oneFilterSelected && gFilterListbox.selectedIndex < gFilterListbox.itemCount - 1 ); gDownButton.disabled = disabled; gBottomButton.disabled = disabled; } /** * Given a selected folder, returns the folder where filters should * be defined (the root folder except for news) if the server can * accept filters. * * @param nsIMsgFolder aFolder - selected folder, from window args * @returns an nsIMsgFolder where the filter is defined */ function getFilterFolderForSelection(aFolder) { let rootFolder = aFolder && aFolder.server ? aFolder.server.rootFolder : null; if (rootFolder && rootFolder.isServer && rootFolder.server.canHaveFilters) { return aFolder.server.type == "nntp" ? aFolder : rootFolder; } return null; } /** * If the selected server cannot have filters, get the default server. * If the default server cannot have filters, check all accounts * and get a server that can have filters. * * @returns an nsIMsgIncomingServer */ function getServerThatCanHaveFilters() { let defaultAccount = MailServices.accounts.defaultAccount; if (defaultAccount) { let defaultIncomingServer = defaultAccount.incomingServer; // Check to see if default server can have filters. if (defaultIncomingServer.canHaveFilters) { return defaultIncomingServer; } } // If it cannot, check all accounts to find a server // that can have filters. return MailServices.accounts.allServers.find(server => server.canHaveFilters); } function onFilterClick(event) { // This is called after the clicked checkbox changed state // so this.checked is the right state we want to toggle to. toggleFilter(this.parentNode, this.checked); } function onFilterDoubleClick(event) { // we only care about button 0 (left click) events if (event.button != 0) { return; } onEditFilter(); } /** * Handles the keypress event on the filter list dialog. * * @param {Event} event - The keypress DOMEvent. */ function onFilterActionButtonKeyPress(event) { if ( event.key == "Enter" || (event.key == " " && event.target.hasAttribute("type")) ) { event.preventDefault(); if ( event.target.classList.contains("toolbarbutton-menubutton-dropmarker") ) { document .getElementById("newFilterMenupopup") .openPopup(event.target.parentNode, "after_end", { triggerEvent: event, }); return; } event.target.click(); } } function onFilterListKeyPress(aEvent) { if (aEvent.keyCode) { switch (aEvent.keyCode) { case KeyEvent.DOM_VK_INSERT: if (!document.getElementById("newButton").disabled) { onNewFilter(); } break; case KeyEvent.DOM_VK_DELETE: if (!document.getElementById("deleteButton").disabled) { onDeleteFilter(); } break; case KeyEvent.DOM_VK_RETURN: if (!document.getElementById("editButton").disabled) { onEditFilter(); } break; } } else if (!aEvent.ctrlKey && !aEvent.altKey && !aEvent.metaKey) { switch (aEvent.charCode) { case KeyEvent.DOM_VK_SPACE: for (let item of gFilterListbox.selectedItems) { toggleFilter(item); } break; default: gSearchBox.focus(); gSearchBox.value = String.fromCharCode(aEvent.charCode); } } } /** * Decides if the given filter matches the given keyword. * * @param aFilter nsIMsgFilter to check * @param aKeyword the string to find in the filter name * * @returns True if the filter name contains the searched keyword. Otherwise false. In the future this may be extended to match other filter attributes. */ function filterSearchMatch(aFilter, aKeyword) { return aFilter.filterName.toLocaleLowerCase().includes(aKeyword); } /** * Called from rebuildFilterList when the list needs to be redrawn. * * @returns Uses the search term in search box, to produce an array of * row (filter) numbers (indexes) that match the search term. */ function onFindFilter() { let keyWord = gSearchBox.value.toLocaleLowerCase(); // If searchbox is empty, just return and let rebuildFilterList // create an unfiltered list. if (!keyWord) { return null; } // Rematch everything in the list, remove what doesn't match the search box. let rows = gCurrentFilterList.filterCount; let matchingFilterList = []; // Use the full gCurrentFilterList, not the filterList listbox, // which may already be filtered. for (let i = 0; i < rows; i++) { if (filterSearchMatch(gCurrentFilterList.getFilterAt(i), keyWord)) { matchingFilterList.push(i); } } return matchingFilterList; } /** * Clear the search term in the search box if needed. * * @param aFilter If this nsIMsgFilter matches the search term, * do not reset the box. If this is null, * reset unconditionally. */ function resetSearchBox(aFilter) { let keyword = gSearchBox.value.toLocaleLowerCase(); if (keyword && (!aFilter || !filterSearchMatch(aFilter, keyword))) { gSearchBox.reset(); } } /** * Display "1 item", "11 items" or "4 of 10" if list is filtered via search box. */ function updateCountBox() { let countBox = document.getElementById("countBox"); let sum = gCurrentFilterList.filterCount; let len = gFilterListbox.itemCount; if (len == sum) { // "N items" countBox.value = PluralForm.get( len, gFilterBundle.getString("filterCountItems") ).replace("#1", len); return; } // "N of M" countBox.value = gFilterBundle.getFormattedString( "filterCountVisibleOfTotal", [len, sum] ); }