1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2011, 2012, 2013, 2014, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * @class 26 * This class represents a search toolbar that shows up above search results. It can be 27 * used to refine the search results. Each search term is contained within a bubble that 28 * can easily be removed. 29 * 30 * @param {hash} params a hash of parameters: 31 * @param {DwtComposite} parent the parent widget 32 * @param {string} id an explicit ID to use for the control's HTML element 33 * 34 * @extends ZmSearchToolBar 35 * 36 * @author Conrad Damon 37 */ 38 ZmSearchResultsToolBar = function(params) { 39 40 params.posStyle = Dwt.ABSOLUTE_STYLE; 41 params.className = "ZmSearchResultsToolBar"; 42 this._controller = params.controller; 43 44 ZmSearchToolBar.apply(this, arguments); 45 46 this._bubbleId = {}; 47 this._origin = ZmId.SEARCHRESULTS; 48 49 this.initAutocomplete(); 50 }; 51 52 ZmSearchResultsToolBar.prototype = new ZmSearchToolBar; 53 ZmSearchResultsToolBar.prototype.constructor = ZmSearchResultsToolBar; 54 55 ZmSearchResultsToolBar.prototype.isZmSearchResultsToolBar = true; 56 ZmSearchResultsToolBar.prototype.toString = function() { return "ZmSearchResultsToolBar"; }; 57 58 59 ZmSearchResultsToolBar.prototype.TEMPLATE = "share.Widgets#ZmSearchResultsToolBar"; 60 61 ZmSearchResultsToolBar.prototype.PENDING_BUBBLE_CONTAINER_CLASS = "addrBubbleContainerPending"; 62 ZmSearchResultsToolBar.prototype.PENDING_SEARCH_DELAY = 2000; // 2 seconds 63 64 ZmSearchResultsToolBar.prototype._createHtml = 65 function() { 66 67 this.getHtmlElement().innerHTML = AjxTemplate.expand(this.TEMPLATE, {id:this._htmlElId}); 68 69 var idContext = this._controller.getCurrentViewId(); 70 71 this._label = document.getElementById(this._htmlElId + "_label"); 72 this._iconDiv = document.getElementById(this._htmlElId + "_icon"); 73 74 // add search input field 75 var inputFieldCellId = this._htmlElId + "_inputFieldCell"; 76 var inputFieldCell = document.getElementById(inputFieldCellId); 77 if (inputFieldCell) { 78 var aifParams = { 79 parent: this, 80 strictMode: false, 81 id: DwtId.makeId(ZmId.WIDGET_INPUT, idContext), 82 bubbleAddedCallback: this._bubbleChange.bind(this), 83 bubbleRemovedCallback: this._bubbleChange.bind(this), 84 noOutsideListening: true, 85 type: ZmId.SEARCH 86 } 87 var aif = this._searchField = new ZmAddressInputField(aifParams); 88 aif.reparentHtmlElement(inputFieldCell); 89 90 var inputEl = this._searchField.getInputElement(); 91 inputEl.className = "search_results_input"; 92 } 93 94 // add search button 95 this._button[ZmSearchToolBar.SEARCH_BUTTON] = ZmToolBar.addButton({ 96 parent: this, 97 tdId: "_searchButton", 98 buttonId: ZmId.getButtonId(idContext, ZmId.SEARCHRESULTS_SEARCH), 99 lbl: ZmMsg.search, 100 tooltip: ZmMsg.searchTooltip 101 }); 102 103 // add save search button if saved-searches enabled 104 this._button[ZmSearchToolBar.SAVE_BUTTON] = ZmToolBar.addButton({ 105 parent: this, 106 setting: ZmSetting.SAVED_SEARCHES_ENABLED, 107 tdId: "_saveButton", 108 buttonId: ZmId.getButtonId(idContext, ZmId.SEARCHRESULTS_SAVE), 109 lbl: ZmMsg.save, 110 tooltip: ZmMsg.saveSearchTooltip 111 }); 112 }; 113 114 // TODO: use the main search toolbar's autocomplete list - need to manage location callback 115 ZmSearchResultsToolBar.prototype.initAutocomplete = 116 function() { 117 if (!this._acList) { 118 this._acList = new ZmAutocompleteListView(this._getAutocompleteParams()); 119 this._acList.handle(this.getSearchField(), this._searchField._htmlElId); 120 } 121 this._searchField.setAutocompleteListView(this._acList); 122 }; 123 124 ZmSearchResultsToolBar.prototype._getAutocompleteParams = 125 function() { 126 var params = ZmSearchToolBar.prototype._getAutocompleteParams.apply(this, arguments); 127 params.options = { noBubbleParse: true }; 128 return params; 129 }; 130 131 ZmSearchResultsToolBar.prototype.setSearch = 132 function(search) { 133 this._settingSearch = true; 134 this._searchField.clear(true); 135 var tokens = search.getTokens(); 136 if (tokens && tokens.length) { 137 for (var i = 0, len = tokens.length; i < len; i++) { 138 var token = tokens[i], prevToken = tokens[i - 1], nextToken = tokens[i + 1]; 139 var showAnd = (prevToken && prevToken.op == ZmParsedQuery.GROUP_CLOSE) || (nextToken && nextToken.op == ZmParsedQuery.GROUP_OPEN); 140 var text = token.toString(showAnd); 141 if (text) { 142 var bubble = this._searchField.addBubble({address:text, noParse:true}); 143 this._bubbleId[text] = bubble.id; 144 } 145 } 146 } 147 this._settingSearch = false; 148 }; 149 150 ZmSearchResultsToolBar.prototype.setLabel = 151 function(text, showError) { 152 this._label.innerHTML = text; 153 this._iconDiv.style.display = showError ? "inline-block" : "none"; 154 }; 155 156 // Don't let the removal or addition of a bubble when we're setting up trigger a search loop. 157 ZmSearchResultsToolBar.prototype._bubbleChange = 158 function(bubble, added) { 159 160 //cancel existing timeout since we restart the 2 seconds wait. 161 this._clearPendingSearchTimeout(); 162 if (this._settingSearch) { 163 return; 164 } 165 var pq = new ZmParsedQuery(bubble.address); 166 var tokens = pq.getTokens(); 167 // don't run search if a conditional was added or removed 168 if (tokens && tokens[0] && tokens[0].type == ZmParsedQuery.COND) { 169 return; 170 } 171 //add class to make the search bar yellow to indicate it's a "pending" state (a.k.a. dirty or edit). 172 Dwt.addClass(this._searchField._elRef, this.PENDING_BUBBLE_CONTAINER_CLASS); 173 // wait 2 seconds before running the search, to see if the user continues to create bubbles (or delete them), so we don't send requests to the server annoying and blocking the user. 174 // Interestingly, I piggy-back on an existing timeout (but that was 10 miliseconds only), This is an old comment as to why there was already a very short timeout ==> 175 // use timer to let content of search bar get set - if a search term is autocompleted, 176 // the bubble is added before the text it replaces is removed 177 this._pendingSearchTimeout = setTimeout(this._handleEnterKeyPress.bind(this), this.PENDING_SEARCH_DELAY); 178 }; 179 180 ZmSearchResultsToolBar.prototype._clearPendingSearchTimeout = 181 function() { 182 if (!this._pendingSearchTimeout) { 183 return; 184 } 185 clearTimeout(this._pendingSearchTimeout); 186 this._pendingSearchTimeout = null; 187 }; 188 189 ZmSearchResultsToolBar.prototype._handleKeyDown = 190 function(ev) { 191 //cancel existing timeout since the user is still typing (new bubble, but not completed yet). 192 this._clearPendingSearchTimeout(); 193 ZmSearchToolBar.prototype._handleKeyDown.apply(this, arguments); 194 }; 195 196 ZmSearchResultsToolBar.prototype._handleEnterKeyPress = 197 function(ev) { 198 //cancel existing timeout in case we explicitly clicked the enter key, so we don't want this to be called twice. 199 this._clearPendingSearchTimeout(); 200 Dwt.delClass(this._searchField._elRef, this.PENDING_BUBBLE_CONTAINER_CLASS); 201 this.setLabel(ZmMsg.searching); 202 ZmSearchToolBar.prototype._handleEnterKeyPress.apply(this, arguments); 203 }; 204 205 /** 206 * Adds a bubble for the given search term. 207 * 208 * @param {ZmSearchToken} term search term 209 * @param {boolean} skipNotify if true, don't trigger a search 210 * @param {boolean} addingCond if true, user clicked to add a conditional term 211 */ 212 ZmSearchResultsToolBar.prototype.addSearchTerm = 213 function(term, skipNotify, addingCond) { 214 215 var text = term.toString(addingCond); 216 var index; 217 if (addingCond) { 218 var bubbleList = this._searchField._getBubbleList(); 219 var bubbles = bubbleList.getArray(); 220 for (var i = 0; i < bubbles.length; i++) { 221 if (bubbleList.isSelected(bubbles[i])) { 222 index = i; 223 break; 224 } 225 } 226 } 227 228 var bubble = this._searchField.addBubble({ 229 address: text, 230 addClass: term.type, 231 skipNotify: skipNotify, 232 index: index 233 }); 234 if (bubble) { 235 this._bubbleId[text] = bubble.id; 236 return bubble.id; 237 } 238 }; 239 240 /** 241 * Removes the bubble with the given search term. 242 * 243 * @param {ZmSearchToken|string} term search term or bubble ID 244 * @param {boolean} skipNotify if true, don't trigger a search 245 */ 246 ZmSearchResultsToolBar.prototype.removeSearchTerm = 247 function(term, skipNotify) { 248 if (!term) { return; } 249 var text = term.toString(); 250 var id = term.isZmSearchToken ? this._bubbleId[text] : term; 251 if (id) { 252 this._searchField.removeBubble(id, skipNotify); 253 delete this._bubbleId[text]; 254 } 255 }; 256