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