1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2010, 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) 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file contains the contact search class.
 27  *
 28  */
 29 
 30 /**
 31  * Creates a component that lets the user search for and select one or more contacts.
 32  * @constructor
 33  * @class
 34  * This class creates and manages a component that lets the user search for and
 35  * select one or more contacts. It is intended to be plugged into a larger
 36  * component such as a dialog or a tab view.
 37  *
 38  * @author Conrad Damon
 39  *
 40  * @param {hash}			params				hash of parameters:
 41  * @param {DwtComposite}		parent			the containing widget
 42  * @param {string}				className		the CSS class
 43  * @param {hash}				options			hash of options:
 44  * @param {string}					preamble	explanatory text
 45  * @param {array}					searchFor	list of ZmContactsApp.SEARCHFOR_*
 46  * @param {boolean}					showEmails	if true, show each email in results
 47  *
 48  * @extends		DwtComposite
 49  *
 50  * TODO: scroll-based paging
 51  * TODO: adapt for contact picker and attachcontacts zimlet
 52  */
 53 ZmContactSearch = function(params) {
 54 
 55 	params = params || {};
 56 	params.parent = params.parent || appCtxt.getShell();
 57 	params.className = params.className || "ZmContactSearch";
 58 	DwtComposite.call(this, params);
 59 
 60 	this._options = params.options;
 61 	this._initialized = false;
 62 	this._searchErrorCallback = new AjxCallback(this, this._handleErrorSearch);
 63 	if (!ZmContactSearch._controller) {
 64 		ZmContactSearch._controller = new ZmContactSearchController();
 65 	}
 66 	this._controller = ZmContactSearch._controller;
 67 	this._initialize();
 68 };
 69 
 70 ZmContactSearch.prototype = new DwtComposite;
 71 ZmContactSearch.prototype.constructor = ZmContactSearch;
 72 
 73 ZmContactSearch.SEARCHFOR_SETTING = {};
 74 ZmContactSearch.SEARCHFOR_SETTING[ZmContactsApp.SEARCHFOR_CONTACTS]	= ZmSetting.CONTACTS_ENABLED;
 75 ZmContactSearch.SEARCHFOR_SETTING[ZmContactsApp.SEARCHFOR_GAL]		= ZmSetting.GAL_ENABLED;
 76 ZmContactSearch.SEARCHFOR_SETTING[ZmContactsApp.SEARCHFOR_PAS]		= ZmSetting.SHARING_ENABLED;
 77 ZmContactSearch.SEARCHFOR_SETTING[ZmContactsApp.SEARCHFOR_FOLDERS]	= ZmSetting.CONTACTS_ENABLED;
 78 
 79 // Public methods
 80 
 81 /**
 82  * Returns a string representation of the object.
 83  *
 84  * @return		{String}		a string representation of the object
 85  */
 86 ZmContactSearch.prototype.toString =
 87 function() {
 88 	return "ZmContactSearch";
 89 };
 90 
 91 /**
 92  * Performs a search.
 93  *
 94  */
 95 ZmContactSearch.prototype.search =
 96 function(ascending, firstTime, lastId, lastSortVal) {
 97 
 98 	if (!AjxUtil.isSpecified(ascending)) {
 99 		ascending = true;
100 	}
101 
102 	var query = this._searchCleared ? AjxStringUtil.trim(this._searchField.value) : "";
103 
104 	var queryHint;
105 	if (this._selectDiv) {
106 		var searchFor = this._selectDiv.getValue();
107 		this._contactSource = (searchFor == ZmContactsApp.SEARCHFOR_CONTACTS || searchFor == ZmContactsApp.SEARCHFOR_PAS)
108 			? ZmItem.CONTACT
109 			: ZmId.SEARCH_GAL;
110 
111 		if (searchFor == ZmContactsApp.SEARCHFOR_PAS) {
112 			queryHint = ZmSearchController.generateQueryForShares(ZmId.ITEM_CONTACT) || "is:local";
113 		} else if (searchFor == ZmContactsApp.SEARCHFOR_CONTACTS) {
114 			queryHint = "is:local";
115 		} else if (searchFor == ZmContactsApp.SEARCHFOR_GAL) {
116             ascending = true;
117         }
118 	} else {
119 		this._contactSource = appCtxt.get(ZmSetting.CONTACTS_ENABLED, null, this._account)
120 			? ZmItem.CONTACT
121 			: ZmId.SEARCH_GAL;
122 
123 		if (this._contactSource == ZmItem.CONTACT) {
124 			queryHint = "is:local";
125 		}
126 	}
127 
128 	this._searchIcon.className = "DwtWait16Icon";
129 
130 	// XXX: line below doesn't have intended effect (turn off column sorting for GAL search)
131 //	this._chooser.sourceListView.sortingEnabled = (this._contactSource == ZmItem.CONTACT);
132 
133 	var params = {
134 		obj: this,
135 		ascending: ascending,
136 		query: query,
137 		queryHint: queryHint,
138 		offset: this._list.size(),
139 		lastId: lastId,
140 		lastSortVal: lastSortVal,
141 		respCallback: (new AjxCallback(this, this._handleResponseSearch, [firstTime])),
142 		errorCallback: this._searchErrorCallback,
143 		accountName: (this._account && this._account.name)
144 	};
145 	ZmContactsHelper.search(params);
146 };
147 
148 ZmContactSearch.prototype._handleResponseSearch =
149 function(firstTime, result) {
150 
151 	this._controller.show(result.getResponse(), firstTime);
152 	var list = this._controller._list;
153 	if (list) {
154 		this._list = list.getVector();
155 		if (list && list.size() == 1) {
156 			this._listView.setSelection(list.get(0));
157 		}
158 	}
159 
160 	this._searchIcon.className = "ImgSearch";
161 	this._searchButton.setEnabled(true);
162 };
163 
164 ZmContactSearch.prototype.getContacts =
165 function() {
166 	return this._listView.getSelection();
167 };
168 
169 ZmContactSearch.prototype.setAccount =
170 function(account) {
171 	if (this._account != account) {
172 		this._account = account;
173 		this._resetSelectDiv();
174 	}
175 };
176 
177 ZmContactSearch.prototype.reset =
178 function(query, account) {
179 
180 	this._offset = 0;
181 	if (this._list) {
182 		this._list.removeAll();
183 	}
184 	this._list = new AjxVector();
185 
186 	// reset search field
187 	this._searchField.disabled = false;
188 	this._searchField.focus();
189 	query = query || this._searchField.value;
190 	if (query) {
191 		this._searchField.className = "";
192 		this._searchField.value = query;
193 		this._searchCleared = true;
194 	} else {
195 		this._searchField.className = "searchFieldHint";
196 		this._searchField.value = ZmMsg.contactPickerHint;
197 		this._searchCleared = false;
198 	}
199 
200 	this.setAccount(account || this._account);
201 };
202 
203 
204 // Private and protected methods
205 
206 ZmContactSearch.prototype._initialize =
207 function() {
208 
209 	this._searchForHash = this._options.searchFor ? AjxUtil.arrayAsHash(this._options.searchFor) : {};
210 
211 	this.getHtmlElement().innerHTML = this._contentHtml();
212 
213 	if (this._options.preamble) {
214 		var div = document.getElementById(this._htmlElId + "_preamble");
215 		div.innerHTML = this._options.preamble;
216 	}
217 
218 	this._searchIcon = document.getElementById(this._htmlElId + "_searchIcon");
219 
220 	// add search button
221 	this._searchButton = new DwtButton({parent:this, parentElement:(this._htmlElId + "_searchButton")});
222 	this._searchButton.setText(ZmMsg.search);
223 	this._searchButton.addSelectionListener(new AjxListener(this, this._searchButtonListener));
224 
225 	// add select menu, if needed
226 	var selectCellId = this._htmlElId + "_folders";
227 	var selectCell = document.getElementById(selectCellId);
228 	if (selectCell) {
229 		this._selectDiv = new DwtSelect({parent:this, parentElement:selectCellId});
230 		this._resetSelectDiv();
231 		this._selectDiv.addChangeListener(new AjxListener(this, this._searchTypeListener));
232 	}
233 
234 	this._searchField = document.getElementById(this._htmlElId + "_searchField");
235 	Dwt.setHandler(this._searchField, DwtEvent.ONKEYUP, ZmContactSearch._keyPressHdlr);
236 	Dwt.setHandler(this._searchField, DwtEvent.ONCLICK, ZmContactSearch._onclickHdlr);
237 	this._keyPressCallback = new AjxCallback(this, this._searchButtonListener);
238 
239 	var listDiv = document.getElementById(this._htmlElId + "_results");
240 	if (listDiv) {
241 		params = {parent:this, parentElement:listDiv, options:this._options};
242 		this._listView = this._controller._listView = new ZmContactSearchListView(params);
243 	}
244 
245 	this._initialized = true;
246 };
247 
248 /**
249  * @private
250  */
251 ZmContactSearch.prototype._contentHtml =
252 function() {
253 
254 	var showSelect;
255 	if (appCtxt.multiAccounts) {
256 		var list = appCtxt.accountList.visibleAccounts;
257 		for (var i = 0; i < list.length; i++) {
258 			this._setSearchFor(list[i]);
259 			if (this._searchFor.length > 1) {
260 				showSelect = true;
261 				break;
262 			}
263 		}
264 	} else {
265 		this._setSearchFor();
266 		showSelect = (this._searchFor.length > 1);
267 	}
268 
269 	var subs = {
270 		id: this._htmlElId,
271 		showSelect: showSelect
272 	};
273 
274 	return (AjxTemplate.expand("abook.Contacts#ZmContactSearch", subs));
275 };
276 
277 ZmContactSearch.prototype._setSearchFor =
278 function(account) {
279 
280 	account = account || this._account;
281 	this._searchFor = [];
282 	if (this._options.searchFor && this._options.searchFor.length) {
283 		for (var i = 0; i < this._options.searchFor.length; i++) {
284 			var searchFor = this._options.searchFor[i];
285 			if (appCtxt.get(ZmContactSearch.SEARCHFOR_SETTING[searchFor], null, account)) {
286 				this._searchFor.push(searchFor);
287 			}
288 		}
289 	}
290 	this._searchForHash = AjxUtil.arrayAsHash(this._searchFor);
291 };
292 
293 /**
294  * @private
295  */
296 ZmContactSearch.prototype._resetSelectDiv =
297 function() {
298 
299 	if (!this._selectDiv) { return; }
300 	
301 	this._selectDiv.clearOptions();
302 	this._setSearchFor();
303 
304 	var sfh = this._searchForHash;
305 	if (sfh[ZmContactsApp.SEARCHFOR_CONTACTS]) {
306 		this._selectDiv.addOption(ZmMsg.contacts, false, ZmContactsApp.SEARCHFOR_CONTACTS);
307 
308 		if (sfh[ZmContactsApp.SEARCHFOR_PAS]) {
309 			this._selectDiv.addOption(ZmMsg.searchPersonalSharedContacts, false, ZmContactsApp.SEARCHFOR_PAS);
310 		}
311 	}
312 
313 	if (sfh[ZmContactsApp.SEARCHFOR_GAL]) {
314 		this._selectDiv.addOption(ZmMsg.GAL, true, ZmContactsApp.SEARCHFOR_GAL);
315 	}
316 
317 	if (!appCtxt.get(ZmSetting.INITIALLY_SEARCH_GAL, null, this._account) ||
318 		!appCtxt.get(ZmSetting.GAL_ENABLED, null, this._account))
319 	{
320 		this._selectDiv.setSelectedValue(ZmContactsApp.SEARCHFOR_CONTACTS);
321 	}
322 
323 	// TODO
324 //	if (sfh[ZmContactsApp.SEARCHFOR_FOLDERS]) {
325 //	}
326 };
327 
328 /**
329  * @private
330  */
331 ZmContactSearch.prototype._searchButtonListener =
332 function(ev) {
333 	this._offset = 0;
334 	this._list.removeAll();
335 	this.search();
336 };
337 
338 /**
339  * @private
340  */
341 ZmContactSearch._keyPressHdlr =
342 function(ev) {
343 	var stb = DwtControl.getTargetControl(ev);
344 	var charCode = DwtKeyEvent.getCharCode(ev);
345 	if (!stb._searchCleared) {
346 		stb._searchField.className = stb._searchField.value = "";
347 		stb._searchCleared = true;
348 	}
349 	if (stb._keyPressCallback && (charCode == 13 || charCode == 3)) {
350 		stb._keyPressCallback.run();
351 		return false;
352 	}
353 	return true;
354 };
355 
356 /**
357  * @private
358  */
359 ZmContactSearch._onclickHdlr =
360 function(ev) {
361 	var stb = DwtControl.getTargetControl(ev);
362 	if (!stb._searchCleared) {
363 		stb._searchField.className = stb._searchField.value = "";
364 		stb._searchCleared = true;
365 	}
366 };
367 
368 
369 // ZmContactSearchController
370 
371 ZmContactSearchController = function(params) {
372 
373 	ZmContactListController.call(this, appCtxt.getShell(), appCtxt.getApp(ZmApp.CONTACTS));
374 };
375 
376 ZmContactSearchController.prototype = new ZmContactListController;
377 ZmContactSearchController.prototype.constructor = ZmContactSearchController;
378 
379 /**
380  * Returns a string representation of the object.
381  *
382  * @return		{String}		a string representation of the object
383  */
384 ZmContactSearchController.prototype.toString =
385 function() {
386 	return "ZmContactSearchController";
387 };
388 
389 ZmContactSearchController.prototype.show =
390 function(searchResult, firstTime) {
391 
392 	var more = searchResult.getAttribute("more");
393 	var list = this._list = searchResult.getResults(ZmItem.CONTACT);
394 	if (list.size() == 0 && firstTime) {
395 		this._listView._setNoResultsHtml();
396 	}
397 
398 	more = more || (this._offset + ZmContactsApp.SEARCHFOR_MAX) < this._list.size();
399 	this._listView.set(list);
400 };
401 
402 // ZmContactSearchListView
403 
404 ZmContactSearchListView = function(params) {
405 
406 	params = params || {};
407 	params.posStyle = Dwt.STATIC_STYLE;
408 	params.className = params.className || "ZmContactSearchListView";
409 	params.headerList = this._getHeaderList();
410 	ZmContactsBaseView.call(this, params);
411 	this._options = params.options;
412 }
413 
414 ZmContactSearchListView.prototype = new ZmContactsBaseView;
415 ZmContactSearchListView.prototype.constructor = ZmContactSearchListView;
416 
417 /**
418  * Returns a string representation of the object.
419  *
420  * @return		{String}		a string representation of the object
421  */
422 ZmContactSearchListView.prototype.toString =
423 function() {
424 	return "ZmContactSearchListView";
425 };
426 
427 ZmContactSearchListView.prototype._getHeaderList =
428 function() {
429 	var headerList = [];
430 	headerList.push(new DwtListHeaderItem({field:ZmItem.F_TYPE, width:ZmMsg.COLUMN_WIDTH_FOLDER_CN}));
431 	headerList.push(new DwtListHeaderItem({field:ZmItem.F_NAME, text:ZmMsg._name, width:ZmMsg.COLUMN_WIDTH_NAME_CN}));
432 	headerList.push(new DwtListHeaderItem({field:ZmItem.F_EMAIL, text:ZmMsg.email}));
433 
434 	return headerList;
435 };
436 
437 /**
438  * @private
439  */
440 ZmContactSearchListView.prototype._getCellContents =
441 function(htmlArr, idx, contact, field, colIdx, params) {
442 	if (field == ZmItem.F_TYPE) {
443 		htmlArr[idx++] = AjxImg.getImageHtml(contact.getIcon());
444 	} else if (field == ZmItem.F_NAME) {
445 		htmlArr[idx++] = AjxStringUtil.htmlEncode(contact.getFileAs());
446 	} else if (field == ZmItem.F_EMAIL) {
447 		htmlArr[idx++] = AjxStringUtil.htmlEncode(contact.getEmail());
448 	}
449 	return idx;
450 };
451