diff options
Diffstat (limited to '')
-rw-r--r-- | comm/mailnews/search/content/searchTerm.js | 568 |
1 files changed, 568 insertions, 0 deletions
diff --git a/comm/mailnews/search/content/searchTerm.js b/comm/mailnews/search/content/searchTerm.js new file mode 100644 index 0000000000..10d085a5e0 --- /dev/null +++ b/comm/mailnews/search/content/searchTerm.js @@ -0,0 +1,568 @@ +/* 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/. */ + +// abSearchDialog.js +/* globals GetScopeForDirectoryURI */ + +var gTotalSearchTerms = 0; +var gSearchTermList; +var gSearchTerms = []; +var gSearchRemovedTerms = []; +var gSearchScope; +var gSearchBooleanRadiogroup; + +var gUniqueSearchTermCounter = 0; // gets bumped every time we add a search term so we can always +// dynamically generate unique IDs for the terms. + +// cache these so we don't have to hit the string bundle for them +var gMoreButtonTooltipText; +var gLessButtonTooltipText; +var gLoading = true; + +function searchTermContainer() {} + +searchTermContainer.prototype = { + internalSearchTerm: "", + internalBooleanAnd: "", + + // this.searchTerm: the actual nsIMsgSearchTerm object + get searchTerm() { + return this.internalSearchTerm; + }, + set searchTerm(val) { + this.internalSearchTerm = val; + + var term = val; + // val is a nsIMsgSearchTerm + var searchAttribute = this.searchattribute; + var searchOperator = this.searchoperator; + var searchValue = this.searchvalue; + + // now reflect all attributes of the searchterm into the widgets + if (searchAttribute) { + // for custom, the value is the custom id, not the integer attribute + if (term.attrib == Ci.nsMsgSearchAttrib.Custom) { + searchAttribute.value = term.customId; + } else { + searchAttribute.value = term.attrib; + } + } + if (searchOperator) { + searchOperator.value = val.op; + } + if (searchValue) { + searchValue.value = term.value; + } + + this.booleanAnd = val.booleanAnd; + this.matchAll = val.matchAll; + }, + + // searchscope - just forward to the searchattribute + get searchScope() { + if (this.searchattribute) { + return this.searchattribute.searchScope; + } + return undefined; + }, + set searchScope(val) { + var searchAttribute = this.searchattribute; + if (searchAttribute) { + searchAttribute.searchScope = val; + } + }, + + saveId(element, slot) { + this[slot] = element.id; + }, + + getElement(slot) { + return document.getElementById(this[slot]); + }, + + // three well-defined properties: + // searchattribute, searchoperator, searchvalue + // the trick going on here is that we're storing the Element's Id, + // not the element itself, because the XBL object may change out + // from underneath us + get searchattribute() { + return this.getElement("internalSearchAttributeId"); + }, + set searchattribute(val) { + this.saveId(val, "internalSearchAttributeId"); + }, + get searchoperator() { + return this.getElement("internalSearchOperatorId"); + }, + set searchoperator(val) { + this.saveId(val, "internalSearchOperatorId"); + }, + get searchvalue() { + return this.getElement("internalSearchValueId"); + }, + set searchvalue(val) { + this.saveId(val, "internalSearchValueId"); + }, + + booleanNodes: null, + get booleanAnd() { + return this.internalBooleanAnd; + }, + set booleanAnd(val) { + this.internalBooleanAnd = val; + }, + + save() { + var searchTerm = this.searchTerm; + var nsMsgSearchAttrib = Ci.nsMsgSearchAttrib; + + if (isNaN(this.searchattribute.value)) { + // is this a custom term? + searchTerm.attrib = nsMsgSearchAttrib.Custom; + searchTerm.customId = this.searchattribute.value; + } else { + searchTerm.attrib = this.searchattribute.value; + } + + if ( + this.searchattribute.value > nsMsgSearchAttrib.OtherHeader && + this.searchattribute.value < nsMsgSearchAttrib.kNumMsgSearchAttributes + ) { + searchTerm.arbitraryHeader = this.searchattribute.label; + } + searchTerm.op = this.searchoperator.value; + if (this.searchvalue.value) { + this.searchvalue.save(); + } else { + this.searchvalue.saveTo(searchTerm.value); + } + searchTerm.value = this.searchvalue.value; + searchTerm.booleanAnd = this.booleanAnd; + searchTerm.matchAll = this.matchAll; + }, + // if you have a search term element with no search term + saveTo(searchTerm) { + this.internalSearchTerm = searchTerm; + this.save(); + }, +}; + +function initializeSearchWidgets() { + gSearchBooleanRadiogroup = document.getElementById("booleanAndGroup"); + gSearchTermList = document.getElementById("searchTermList"); + + // initialize some strings + var bundle = Services.strings.createBundle( + "chrome://messenger/locale/search.properties" + ); + gMoreButtonTooltipText = bundle.GetStringFromName("moreButtonTooltipText"); + gLessButtonTooltipText = bundle.GetStringFromName("lessButtonTooltipText"); +} + +function initializeBooleanWidgets() { + var booleanAnd = true; + var matchAll = false; + // get the boolean value from the first term + var firstTerm = gSearchTerms[0].searchTerm; + if (firstTerm) { + // If there is a second term, it should actually define whether we're + // using 'and' or not. Note that our UI is not as rich as the + // underlying search model, so there's the potential to lose here when + // grouping is involved. + booleanAnd = + gSearchTerms.length > 1 + ? gSearchTerms[1].searchTerm.booleanAnd + : firstTerm.booleanAnd; + matchAll = firstTerm.matchAll; + } + // target radio items have value="and" or value="or" or "all" + if (matchAll) { + gSearchBooleanRadiogroup.value = "matchAll"; + } else if (booleanAnd) { + gSearchBooleanRadiogroup.value = "and"; + } else { + gSearchBooleanRadiogroup.value = "or"; + } + var searchTerms = document.getElementById("searchTermList"); + if (searchTerms) { + updateSearchTermsListbox(matchAll); + } +} + +function initializeSearchRows(scope, searchTerms) { + for (let i = 0; i < searchTerms.length; i++) { + let searchTerm = searchTerms[i]; + createSearchRow(i, scope, searchTerm, false); + gTotalSearchTerms++; + } + initializeBooleanWidgets(); + updateRemoveRowButton(); +} + +/** + * Enables/disables all the visible elements inside the search terms listbox. + * + * @param matchAllValue boolean value from the first search term + */ +function updateSearchTermsListbox(matchAllValue) { + var searchTerms = document.getElementById("searchTermList"); + searchTerms.setAttribute("disabled", matchAllValue); + var searchAttributeList = + searchTerms.getElementsByTagName("search-attribute"); + var searchOperatorList = searchTerms.getElementsByTagName("search-operator"); + var searchValueList = searchTerms.getElementsByTagName("search-value"); + for (let i = 0; i < searchAttributeList.length; i++) { + searchAttributeList[i].setAttribute("disabled", matchAllValue); + searchOperatorList[i].setAttribute("disabled", matchAllValue); + searchValueList[i].setAttribute("disabled", matchAllValue); + if (!matchAllValue) { + searchValueList[i].removeAttribute("disabled"); + } + } + var moreOrLessButtonsList = searchTerms.getElementsByTagName("button"); + for (let i = 0; i < moreOrLessButtonsList.length; i++) { + moreOrLessButtonsList[i].setAttribute("disabled", matchAllValue); + } + if (!matchAllValue) { + updateRemoveRowButton(); + } +} + +// enables/disables the less button for the first row of search terms. +function updateRemoveRowButton() { + var firstListItem = gSearchTermList.getItemAtIndex(0); + if (firstListItem) { + firstListItem.lastElementChild.lastElementChild.setAttribute( + "disabled", + gTotalSearchTerms == 1 + ); + } +} + +// Returns the actual list item row index in the list of search rows +// that contains the passed in element id. +function getSearchRowIndexForElement(aElement) { + var listItem = aElement; + + while (listItem && listItem.localName != "richlistitem") { + listItem = listItem.parentNode; + } + + return gSearchTermList.getIndexOfItem(listItem); +} + +function onMore(event) { + // if we have an event, extract the list row index and use that as the row number + // for our insertion point. If there is no event, append to the end.... + var rowIndex; + + if (event) { + rowIndex = getSearchRowIndexForElement(event.target) + 1; + } else { + rowIndex = gSearchTermList.getRowCount(); + } + + createSearchRow(rowIndex, gSearchScope, null, event != null); + gTotalSearchTerms++; + updateRemoveRowButton(); + + // the user just added a term, so scroll to it + gSearchTermList.ensureIndexIsVisible(rowIndex); +} + +function onLess(event) { + if (event && gTotalSearchTerms > 1) { + removeSearchRow(getSearchRowIndexForElement(event.target)); + --gTotalSearchTerms; + } + + updateRemoveRowButton(); +} + +// set scope on all visible searchattribute tags +function setSearchScope(scope) { + gSearchScope = scope; + for (var i = 0; i < gSearchTerms.length; i++) { + // don't set element attributes if XBL hasn't loaded + if (!(gSearchTerms[i].obj.searchattribute.searchScope === undefined)) { + gSearchTerms[i].obj.searchattribute.searchScope = scope; + // act like the user "selected" this, see bug #202848 + gSearchTerms[i].obj.searchattribute.onSelect(null /* no event */); + } + gSearchTerms[i].scope = scope; + } +} + +function updateSearchAttributes() { + for (var i = 0; i < gSearchTerms.length; i++) { + gSearchTerms[i].obj.searchattribute.refreshList(); + } +} + +function booleanChanged(event) { + // when boolean changes, we have to update all the attributes on the search terms + var newBoolValue = event.target.getAttribute("value") == "and"; + var matchAllValue = event.target.getAttribute("value") == "matchAll"; + if (document.getElementById("abPopup")) { + var selectedAB = document.getElementById("abPopup").selectedItem.value; + setSearchScope(GetScopeForDirectoryURI(selectedAB)); + } + for (var i = 0; i < gSearchTerms.length; i++) { + let searchTerm = gSearchTerms[i].obj; + // If term is not yet initialized in the UI, change the original object. + if (!searchTerm || !gSearchTerms[i].initialized) { + searchTerm = gSearchTerms[i].searchTerm; + } + + searchTerm.booleanAnd = newBoolValue; + searchTerm.matchAll = matchAllValue; + } + var searchTerms = document.getElementById("searchTermList"); + if (searchTerms) { + if (!matchAllValue && searchTerms.hidden && !gTotalSearchTerms) { + // Fake to get empty row. + onMore(null); + } + updateSearchTermsListbox(matchAllValue); + } +} + +/** + * Create a new search row with all the needed elements. + * + * @param index index of the position in the menulist where to add the row + * @param scope a nsMsgSearchScope constant indicating scope of this search rule + * @param searchTerm nsIMsgSearchTerm object to hold the search term + * @param aUserAdded boolean indicating if the row addition was initiated by the user + * (e.g. via the '+' button) + */ +function createSearchRow(index, scope, searchTerm, aUserAdded) { + var searchAttr = document.createXULElement("search-attribute"); + var searchOp = document.createXULElement("search-operator"); + var searchVal = document.createXULElement("search-value"); + + var moreButton = document.createXULElement("button"); + var lessButton = document.createXULElement("button"); + moreButton.setAttribute("class", "small-button"); + moreButton.setAttribute("oncommand", "onMore(event);"); + moreButton.setAttribute("label", "+"); + moreButton.setAttribute("tooltiptext", gMoreButtonTooltipText); + lessButton.setAttribute("class", "small-button"); + lessButton.setAttribute("oncommand", "onLess(event);"); + lessButton.setAttribute("label", "\u2212"); + lessButton.setAttribute("tooltiptext", gLessButtonTooltipText); + + // now set up ids: + searchAttr.id = "searchAttr" + gUniqueSearchTermCounter; + searchOp.id = "searchOp" + gUniqueSearchTermCounter; + searchVal.id = "searchVal" + gUniqueSearchTermCounter; + + searchAttr.setAttribute("for", searchOp.id + "," + searchVal.id); + searchOp.setAttribute("opfor", searchVal.id); + + var rowdata = [searchAttr, searchOp, searchVal, [moreButton, lessButton]]; + var searchrow = constructRow(rowdata); + searchrow.id = "searchRow" + gUniqueSearchTermCounter; + + var searchTermObj = new searchTermContainer(); + searchTermObj.searchattribute = searchAttr; + searchTermObj.searchoperator = searchOp; + searchTermObj.searchvalue = searchVal; + + // now insert the new search term into our list of terms + gSearchTerms.splice(index, 0, { + obj: searchTermObj, + scope, + searchTerm, + initialized: false, + }); + + var editFilter = window.gFilter || null; + var editMailView = window.gMailView || null; + + if ( + (!editFilter && !editMailView) || + (editFilter && index == gTotalSearchTerms) || + (editMailView && index == gTotalSearchTerms) + ) { + gLoading = false; + } + + // index is index of new row + // gTotalSearchTerms has not been updated yet + if (gLoading || index == gTotalSearchTerms) { + gSearchTermList.appendChild(searchrow); + } else { + var currentItem = gSearchTermList.getItemAtIndex(index); + gSearchTermList.insertBefore(searchrow, currentItem); + } + + // If this row was added by user action, focus the value field. + if (aUserAdded) { + document.commandDispatcher.advanceFocusIntoSubtree(searchVal); + searchrow.setAttribute("highlight", "true"); + } + + // bump our unique search term counter + gUniqueSearchTermCounter++; +} + +function initializeTermFromId(id) { + initializeTermFromIndex( + getSearchRowIndexForElement(document.getElementById(id)) + ); +} + +function initializeTermFromIndex(index) { + var searchTermObj = gSearchTerms[index].obj; + + searchTermObj.searchScope = gSearchTerms[index].scope; + // the search term will initialize the searchTerm element, including + // .booleanAnd + if (gSearchTerms[index].searchTerm) { + searchTermObj.searchTerm = gSearchTerms[index].searchTerm; + // here, we don't have a searchTerm, so it's probably a new element - + // we'll initialize the .booleanAnd from the existing setting in + // the UI + } else { + searchTermObj.booleanAnd = gSearchBooleanRadiogroup.value == "and"; + if (index) { + // If we weren't pre-initialized with a searchTerm then steal the + // search attribute and operator from the previous row. + searchTermObj.searchattribute.value = + gSearchTerms[index - 1].obj.searchattribute.value; + searchTermObj.searchoperator.value = + gSearchTerms[index - 1].obj.searchoperator.value; + } + } + + gSearchTerms[index].initialized = true; +} + +/** + * Creates a <richlistitem> using the array children as the children + * of each listcell. + * + * @param aChildren An array of XUL elements to put into the listitem. + * Each array member is put into a separate listcell. + * If the member itself is an array of elements, + * all of them are put into the same listcell. + */ +function constructRow(aChildren) { + let cols = gSearchTermList.firstElementChild.children; // treecol elements + let listitem = document.createXULElement("richlistitem"); + listitem.setAttribute("allowevents", "true"); + for (let i = 0; i < aChildren.length; i++) { + let listcell = document.createXULElement("hbox"); + if (cols[i].hasAttribute("style")) { + listcell.setAttribute("style", cols[i].getAttribute("style")); + } + let child = aChildren[i]; + + if (child instanceof Array) { + for (let j = 0; j < child.length; j++) { + listcell.appendChild(child[j]); + } + } else { + child.setAttribute("flex", "1"); + listcell.appendChild(child); + } + listitem.appendChild(listcell); + } + return listitem; +} + +function removeSearchRow(index) { + var searchTermObj = gSearchTerms[index].obj; + if (!searchTermObj) { + return; + } + + // if it is an existing (but offscreen) term, + // make sure it is initialized before we remove it. + if (!gSearchTerms[index].searchTerm && !gSearchTerms[index].initialized) { + initializeTermFromIndex(index); + } + + // need to remove row from list, so walk upwards from the + // searchattribute to find the first <listitem> + var listitem = searchTermObj.searchattribute; + + while (listitem) { + if (listitem.localName == "richlistitem") { + break; + } + listitem = listitem.parentNode; + } + + if (!listitem) { + dump("Error: couldn't find parent listitem!\n"); + return; + } + + if (searchTermObj.searchTerm) { + gSearchRemovedTerms.push(searchTermObj.searchTerm); + } else { + // dump("That wasn't real. ignoring \n"); + } + + listitem.remove(); + + // now remove the item from our list of terms + gSearchTerms.splice(index, 1); +} + +/** + * Save the search terms from the UI back to the actual search terms. + * + * @param {nsIMsgSearchTerm[]} searchTerms - Array of terms + * @param {object} termOwner - Object which can contain and create the terms + * e.g. a nsIMsgSearchSession (will be unnecessary if we just make terms + * creatable via XPCOM). + * @returns {nsIMsgSearchTerm[]} The filtered searchTerms. + */ +function saveSearchTerms(searchTerms, termOwner) { + var matchAll = gSearchBooleanRadiogroup.value == "matchAll"; + var i; + + searchTerms = searchTerms.filter(t => !gSearchRemovedTerms.includes(t)); + + for (i = 0; i < gSearchTerms.length; i++) { + try { + gSearchTerms[i].obj.matchAll = matchAll; + var searchTerm = gSearchTerms[i].obj.searchTerm; + if (searchTerm) { + gSearchTerms[i].obj.save(); + } else if (!gSearchTerms[i].initialized) { + // the term might be an offscreen one we haven't initialized yet + searchTerm = gSearchTerms[i].searchTerm; + } else { + // need to create a new searchTerm, and somehow save it to that + searchTerm = termOwner.createTerm(); + gSearchTerms[i].obj.saveTo(searchTerm); + // this might not be the right place for the term, + // but we need to make the array longer anyway + termOwner.appendTerm(searchTerm); + } + searchTerms[i] = searchTerm; + } catch (ex) { + dump("** Error saving element " + i + ": " + ex + "\n"); + } + } + return searchTerms; +} + +function onReset(event) { + while (gTotalSearchTerms > 0) { + removeSearchRow(--gTotalSearchTerms); + } + onMore(null); +} + +function hideMatchAllItem() { + var allItems = document.getElementById("matchAllItem"); + if (allItems) { + allItems.hidden = true; + } +} |