1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 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) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * @overview 26 * This file contains the contacts base view classes. 27 */ 28 29 /** 30 * Creates the base view. 31 * @class 32 * This class represents the base view. 33 * 34 * @param {Hash} params a hash of parameters 35 * 36 * @extends ZmListView 37 */ 38 ZmContactsBaseView = function(params) { 39 40 if (arguments.length == 0) { return; } 41 42 params.posStyle = params.posStyle || Dwt.ABSOLUTE_STYLE; 43 params.type = ZmItem.CONTACT; 44 params.pageless = true; 45 ZmListView.call(this, params); 46 47 this._handleEventType[ZmItem.GROUP] = true; 48 }; 49 50 ZmContactsBaseView.prototype = new ZmListView; 51 ZmContactsBaseView.prototype.constructor = ZmContactsBaseView; 52 53 /** 54 * Returns a string representation of the object. 55 * 56 * @return {String} a string representation of the object 57 */ 58 ZmContactsBaseView.prototype.toString = 59 function() { 60 return "ZmContactsBaseView"; 61 }; 62 63 /** 64 * Sets the list. 65 * 66 * @param {ZmContactList} list the list 67 * @param {String} sortField the sort field 68 * @param {String} folderId the folder id 69 */ 70 ZmContactsBaseView.prototype.set = 71 function(list, sortField, folderId) { 72 73 if (this._itemsToAdd) { 74 this.addItems(this._itemsToAdd); 75 this._itemsToAdd = null; 76 } else { 77 var subList; 78 if (list instanceof ZmContactList) { 79 // compute the sublist based on the folderId if applicable 80 list.addChangeListener(this._listChangeListener); 81 // for accounts where gal paging is not supported, show *all* results 82 subList = (list.isGal && !list.isGalPagingSupported) 83 ? list.getVector().clone() 84 : list.getSubList(this.offset, this.getLimit(this.offset), folderId); 85 } else { 86 subList = list; 87 } 88 this._folderId = folderId; 89 DwtListView.prototype.set.call(this, subList, sortField); 90 } 91 this._setRowHeight(); 92 this._rendered = true; 93 }; 94 95 /** 96 * @private 97 */ 98 ZmContactsBaseView.prototype._setParticipantToolTip = 99 function(address) { 100 // XXX: OVERLOADED TO SUPPRESS JS ERRORS.. 101 // XXX: REMOVE WHEN IMPLEMENTED - SEE BASE CLASS ZmListView 102 }; 103 104 /** 105 * Gets the list view. 106 * 107 * @return {ZmContactsBaseView} the list view 108 */ 109 ZmContactsBaseView.prototype.getListView = 110 function() { 111 return this; 112 }; 113 114 /** 115 * Gets the title. 116 * 117 * @return {String} the view title 118 */ 119 ZmContactsBaseView.prototype.getTitle = 120 function() { 121 return [ZmMsg.zimbraTitle, this._controller.getApp().getDisplayName()].join(": "); 122 }; 123 124 /** 125 * @private 126 */ 127 ZmContactsBaseView.prototype._changeListener = 128 function(ev) { 129 var folderId = this._controller.getFolderId(); 130 131 // if we dont have a folder, then assume user did a search of contacts 132 if (folderId != null || ev.event != ZmEvent.E_MOVE) { 133 ZmListView.prototype._changeListener.call(this, ev); 134 135 if (ev.event == ZmEvent.E_MODIFY) { 136 this._modifyContact(ev); 137 var contact = ev.item || ev._details.items[0]; 138 if (contact instanceof ZmContact) { 139 this.setSelection(contact, false, true); 140 } 141 } else if (ev.event == ZmEvent.E_CREATE) { 142 var newContact = ev._details.items[0]; 143 var newFolder = appCtxt.getById(newContact.folderId); 144 var newFolderId = newFolder && (appCtxt.getActiveAccount().isMain ? newFolder.nId : newFolder.id); 145 var visible = ev.getDetail("visible"); 146 147 // only add this new contact to the listview if this is a simple 148 // folder search and it belongs! 149 if (folderId && newFolder && folderId == newFolderId && visible) { 150 var index = ev.getDetail("sortIndex"); 151 var alphaBar = this.parent ? this.parent.getAlphabetBar() : null; 152 var inAlphaBar = alphaBar ? alphaBar.isItemInAlphabetLetter(newContact) : true; 153 if (index != null && inAlphaBar) { 154 this.addItem(newContact, index); 155 } 156 157 // always select newly added contact if its been added to the 158 // current page of contacts 159 if (inAlphaBar) { 160 this.setSelection(newContact, false, true); 161 } 162 } 163 } else if (ev.event == ZmEvent.E_DELETE) { 164 // bug fix #19308 - do house-keeping on controller's list so 165 // replenishment works as it should 166 var list = this._controller.getList(); 167 if (list) { 168 list.remove(ev.item); 169 } 170 } 171 } 172 }; 173 174 ZmContactsBaseView.prototype.setSelection = 175 function(item, skipNotify, setPending) { 176 if (!item) { return; } 177 178 var el = this._getElFromItem(item); 179 if (el) { 180 ZmListView.prototype.setSelection.call(this, item, skipNotify); 181 this._pendingSelection = null; 182 } else if (setPending) { 183 this._pendingSelection = {item: item, skipNotify: skipNotify}; 184 } 185 }; 186 187 ZmContactsBaseView.prototype.addItems = 188 function(itemArray) { 189 ZmListView.prototype.addItems.call(this, itemArray); 190 if (this._pendingSelection && AjxUtil.indexOf(itemArray, this._pendingSelection.item)!=-1) { 191 this.setSelection(this._pendingSelection.item, this._pendingSelection.skipNotify); 192 } 193 } 194 195 196 /** 197 * @private 198 */ 199 ZmContactsBaseView.prototype._modifyContact = 200 function(ev) { 201 var list = this.getList(); 202 //the item was updated - the list might be "old" (not pointing to the latest items, 203 // since we refreshed the items in the appCtxt cache by a different view. see bug 84226) 204 //therefor let's make sure the modified contact replaces the old one in the list. 205 var contact = ev.item; 206 if (contact) { 207 var arr = list.getArray(); 208 for (var i = 0; i < arr.length; i++) { 209 if (arr[i].id === contact.id) { 210 if (arr[i] === contact) { 211 //nothing changed, still points to same object 212 break; 213 } 214 arr[i] = contact; 215 //update the viewed contact 216 this.parent.setContact(contact); 217 break; 218 } 219 } 220 } 221 // if fileAs changed, resort the internal list 222 // XXX: this is somewhat inefficient. We should just remove this contact and reinsert 223 if (ev.getDetail("fileAsChanged")) { 224 if (list) { 225 list.sort(ZmContact.compareByFileAs); 226 } 227 } 228 }; 229 230 /** 231 * @private 232 */ 233 ZmContactsBaseView.prototype._setNextSelection = 234 function() { 235 // set the next appropriate selected item 236 if (this.firstSelIndex < 0) { 237 this.firstSelIndex = 0; 238 } 239 240 // get first valid item to select 241 var item; 242 if (this._list) { 243 item = this._list.get(this.firstSelIndex); 244 245 // only get the first non-trash contact to select if we're not in Trash 246 if (this._controller.getFolderId() == ZmFolder.ID_TRASH) { 247 if (!item) { 248 item = this._list.get(0); 249 } 250 } else if (item == null || (item && item.folderId == ZmFolder.ID_TRASH)) { 251 item = null; 252 var list = this._list.getArray(); 253 254 if (this.firstSelIndex > 0 && this.firstSelIndex == list.length) { 255 item = list[list.length-1]; 256 } else { 257 for (var i=0; i < list.length; i++) { 258 if (list[i].folderId != ZmFolder.ID_TRASH) { 259 item = list[i]; 260 break; 261 } 262 } 263 } 264 265 // reset first sel index 266 if (item) { 267 var div = document.getElementById(this._getItemId(item)); 268 if (div) { 269 var data = this._data[div.id]; 270 this.firstSelIndex = this._list ? this._list.indexOf(data.item) : -1; 271 } 272 } 273 } 274 } 275 276 this.setSelection(item); 277 }; 278 279 /** 280 * Creates the alphabet bar. 281 * @class 282 * This class represents the contact alphabet bar. 283 * 284 * @param {DwtComposite} parent the parent 285 * 286 * @extends DwtComposite 287 */ 288 ZmContactAlphabetBar = function(parent) { 289 290 DwtComposite.call(this, {parent:parent}); 291 292 this._createHtml(); 293 294 this._all = this._current = document.getElementById(this._alphabetBarId).rows[0].cells[0]; 295 this._currentLetter = null; 296 this.setSelected(this._all, true); 297 this._enabled = true; 298 this.addListener(DwtEvent.ONCLICK, this._onClick.bind(this)); 299 }; 300 301 ZmContactAlphabetBar.prototype = new DwtComposite; 302 ZmContactAlphabetBar.prototype.constructor = ZmContactAlphabetBar; 303 ZmContactAlphabetBar.prototype.role = 'toolbar'; 304 305 /** 306 * Returns a string representation of the object. 307 * 308 * @return {String} a string representation of the object 309 */ 310 ZmContactAlphabetBar.prototype.toString = 311 function() { 312 return "ZmContactAlphabetBar"; 313 }; 314 315 /** 316 * Enables the bar. 317 * 318 * @param {Boolean} enable if <code>true</code>, enable the bar 319 */ 320 ZmContactAlphabetBar.prototype.enable = 321 function(enable) { 322 this._enabled = enable; 323 324 var alphabetBarEl = document.getElementById(this._alphabetBarId); 325 if (alphabetBarEl) { 326 alphabetBarEl.className = enable ? "AlphabetBarTable" : "AlphabetBarTable AlphabetBarDisabled"; 327 } 328 }; 329 330 /** 331 * Checks if the bar is enabled. 332 * 333 * @return {Boolean} <code>true</code> if enabled 334 */ 335 ZmContactAlphabetBar.prototype.enabled = 336 function() { 337 return this._enabled; 338 }; 339 340 /** 341 * Resets the bar. 342 * 343 * @param {Object} useCell the cell or <code>null</code> 344 * @return {Boolean} Whether the cell was changed (false if it was already set to useCell) 345 */ 346 ZmContactAlphabetBar.prototype.reset = 347 function(useCell) { 348 var cell = useCell || this._all; 349 if (cell != this._current) { 350 this.setSelected(this._current, false); 351 this._current = cell; 352 this._currentLetter = useCell && useCell != this._all ? useCell.innerHTML : null; 353 this.setSelected(cell, true); 354 return true; 355 } 356 return false; 357 }; 358 359 /** 360 * Sets the button index. 361 * 362 * @param {int} index the index 363 */ 364 ZmContactAlphabetBar.prototype.setButtonByIndex = 365 function(index) { 366 var table = document.getElementById(this._alphabetBarId); 367 var cell = table.rows[0].cells[index]; 368 if (cell) { 369 this.reset(cell); 370 } 371 }; 372 373 /** 374 * Gets the current cell. 375 * 376 * @return {Object} the cell 377 */ 378 ZmContactAlphabetBar.prototype.getCurrent = 379 function() { 380 return this._current; 381 }; 382 383 /** 384 * Gets the current cell letter. 385 * 386 * @return {String} the cell letter, or null for "all" 387 */ 388 ZmContactAlphabetBar.prototype.getCurrentLetter = 389 function() { 390 return this._currentLetter; 391 }; 392 393 /** 394 * Sets the cell as selected. 395 * 396 * @param {Object} cell the cell 397 * @param {Boolean} selected if <code>true</code>, set as selected 398 */ 399 ZmContactAlphabetBar.prototype.setSelected = 400 function(cell, selected) { 401 cell.className = selected 402 ? "DwtButton-active AlphabetBarCell" 403 : "DwtButton AlphabetBarCell"; 404 cell.setAttribute('aria-selected', selected); 405 if (selected) { 406 this.getHtmlElement().setAttribute('aria-activedescendant', cell.id); 407 this.setFocusElement(cell); 408 } 409 }; 410 411 /** 412 * Sets the cell as selected and performs a new search based on the selection. 413 * 414 * @param {Object} cell the cell 415 * @param {String} letter the letter to begin the search with 416 * @param {String} endLetter the letter to end the search with 417 */ 418 ZmContactAlphabetBar.alphabetClicked = 419 function(cell, letter, endLetter) { 420 // get reference to alphabet bar - ugh 421 var clc = AjxDispatcher.run("GetContactListController"); 422 var alphabetBar = clc && clc.getCurrentView() && clc.getCurrentView().getAlphabetBar(); 423 if (alphabetBar && alphabetBar.enabled()) { 424 if (alphabetBar.reset(cell)) { 425 letter = letter && String(letter).substr(0,1); 426 endLetter = endLetter && String(endLetter).substr(0,1); 427 clc.searchAlphabet(letter, endLetter); 428 } 429 } 430 }; 431 432 /** 433 * determine if contact belongs in the current alphabet bar. Used when creating a new contact and not doing a reload -- 434 * such as new contact group from action menu. 435 * @param item {ZmContact} 436 * @return {boolean} true/false if item belongs in alphabet selection 437 */ 438 ZmContactAlphabetBar.prototype.isItemInAlphabetLetter = 439 function(item) { 440 var inCurrentBar = false; 441 if (item) { 442 if (ZmMsg.alphabet && ZmMsg.alphabet.length > 0) { 443 var all = ZmMsg.alphabet.split(",")[0]; //get "All" for locale 444 } 445 var fileAs = item.getFileAs(); 446 var currentLetter = this.getCurrentLetter(); 447 if (!currentLetter || currentLetter.toLowerCase() == all) { 448 inCurrentBar = true; //All is selected 449 } 450 else if (currentLetter && fileAs) { 451 var itemLetter = String(fileAs).substr(0,1).toLowerCase(); 452 var cellLetter = currentLetter.substr(0,1).toLowerCase(); 453 if (itemLetter == cellLetter) { 454 inCurrentBar = true; 455 } 456 else if(AjxStringUtil.isDigit(cellLetter) && AjxStringUtil.isDigit(itemLetter)) { 457 //handles "123" in alphabet bar 458 inCurrentBar = true; 459 } 460 else if (currentLetter.toLowerCase() == "a-z" && itemLetter.match("[a-z]")) { 461 //handle A-Z cases for certain locales 462 inCurrentBar = true; 463 } 464 } 465 } 466 return inCurrentBar; 467 }; 468 469 /** 470 * @private 471 */ 472 ZmContactAlphabetBar.prototype._createHtml = 473 function() { 474 this._alphabetBarId = this._htmlElId + "_alphabet"; 475 var alphabet = ZmMsg.alphabet.split(","); 476 477 this.startSortMap = 478 ZmContactAlphabetBar._parseSortVal(ZmMsg.alphabetSortValue); 479 480 this.endSortMap = 481 ZmContactAlphabetBar._parseSortVal(ZmMsg.alphabetEndSortValue); 482 483 var subs = { 484 id: this._htmlElId, 485 alphabet: alphabet, 486 numLetters: alphabet.length 487 }; 488 489 var element = this.getHtmlElement(); 490 element.innerHTML = AjxTemplate.expand("abook.Contacts#ZmAlphabetBar", subs); 491 this.setAttribute('aria-label', ZmMsg.alphabetLabel); 492 493 AjxUtil.foreach(Dwt.byClassName('AlphabetBarCell', element), (function(cell) { 494 this._makeFocusable(cell, true); 495 this._setEventHdlrs([ DwtEvent.ONCLICK ], false, cell); 496 }).bind(this)); 497 498 // IE8 doesn't support :last-child selector 499 if (AjxEnv.isIE8) { 500 var lastCell = Dwt.byClassName('AlphabetBarCell', element).pop(); 501 Dwt.addClass(lastCell, 'AlphabetBarLastCell'); 502 } 503 }; 504 505 ZmContactAlphabetBar.prototype.getInputElement = 506 function() { 507 return this._current; 508 }; 509 510 ZmContactAlphabetBar.prototype.getKeyMapName = 511 function() { 512 return DwtKeyMap.MAP_TOOLBAR_HORIZ; 513 }; 514 515 ZmContactAlphabetBar.prototype.handleKeyAction = 516 function(actionCode, ev) { 517 var target = 518 Dwt.hasClass(ev.target, 'AlphabetBarCell') ? ev.target : this._current; 519 520 switch (actionCode) { 521 case DwtKeyMap.PREV: 522 var previous = Dwt.getPreviousElementSibling(target); 523 if (previous) { 524 this.setFocusElement(previous); 525 } 526 return true; 527 528 case DwtKeyMap.NEXT: 529 var next = Dwt.getNextElementSibling(target); 530 if (next) { 531 this.setFocusElement(next); 532 } 533 return true; 534 535 case DwtKeyMap.SELECT: 536 target.click(); 537 return true; 538 } 539 }; 540 541 ZmContactAlphabetBar._parseSortVal = 542 function(sortVal) { 543 if (!sortVal) { 544 return {}; 545 } 546 var sortMap = {}; 547 var values = sortVal.split(","); 548 if (values && values.length) { 549 for (var i = 0; i < values.length; i++) { 550 var parts = values[i].split(":"); 551 sortMap[parts[0]] = parts[1]; 552 } 553 } 554 return sortMap; 555 }; 556 557 /** 558 * @private 559 */ 560 ZmContactAlphabetBar.prototype._onClick = 561 function(ev) { 562 var cell = DwtUiEvent.getTarget(ev); 563 564 if (!Dwt.hasClass(cell, 'AlphabetBarCell') || 565 !this.enabled() || !this.reset(cell)) { 566 return; 567 } 568 569 var idx = AjxUtil.indexOf(cell.parentNode.children, cell); 570 var alphabet = ZmMsg.alphabet.split(","); 571 572 var startLetter = null, endLetter = null; 573 574 if (idx > 0) { 575 startLetter = this.startSortMap[alphabet[idx]] || alphabet[idx].substr(0, 1); 576 577 if (idx < alphabet.length - 1) { 578 endLetter = this.endSortMap[alphabet[idx]] || alphabet[idx + 1].substr(0, 1); 579 } 580 } 581 582 var clc = AjxDispatcher.run("GetContactListController"); 583 clc.searchAlphabet(startLetter, endLetter); 584 }; 585 586 587