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