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 * 27 */ 28 29 /** 30 * Creates a list view. 31 * @class 32 * A list view presents a list of items as rows with fields (columns). 33 * 34 * @author Parag Shah 35 * @author Conrad Damon 36 * 37 * @param {Hash} params a hash of parameters 38 * @param {DwtComposite} params.parent the parent widget 39 * @param {String} params.className the CSS class 40 * @param {constant} params.posStyle the positioning style 41 * @param {String} params.id the HTML ID for element 42 * @param {Array} params.headerList the list of IDs for columns 43 * @param {Boolean} params.noMaximize if <code>true</code>, all columns are fixed-width (otherwise, one will expand to fill available space) 44 * @param {constant} params.view the ID of view 45 * @param {constant} params.type the type of item displayed 46 * @param {ZmListController} params.controller the owning controller 47 * @param {DwtDropTarget} params.dropTgt the drop target 48 * @param {Boolean} params.pageless if <code>true</code>, enlarge page via scroll rather than pagination 49 * 50 * @extends DwtListView 51 */ 52 ZmListView = function(params) { 53 54 if (arguments.length == 0) { return; } 55 56 params.id = params.id || ZmId.getViewId(params.view); 57 DwtListView.call(this, params); 58 59 this.view = params.view; 60 this.type = params.type; 61 this._controller = params.controller; 62 this.setDropTarget(params.dropTgt); 63 64 // create listeners for changes to the list model, folder tree, and tag list 65 this._listChangeListener = new AjxListener(this, this._changeListener); 66 this._tagListChangeListener = new AjxListener(this, this._tagChangeListener); 67 var tagList = appCtxt.getTagTree(); 68 if (tagList) { 69 tagList.addChangeListener(this._tagListChangeListener); 70 } 71 var folderTree = appCtxt.getFolderTree(); 72 if (folderTree) { 73 this._boundFolderChangeListener = this._folderChangeListener.bind(this); 74 folderTree.addChangeListener(this._boundFolderChangeListener); 75 } 76 77 this._handleEventType = {}; 78 this._handleEventType[this.type] = true; 79 this._disallowSelection = {}; 80 this._disallowSelection[ZmItem.F_FLAG] = true; 81 this._disallowSelection[ZmItem.F_MSG_PRIORITY] = true; 82 this._selectAllEnabled = false; 83 84 if (params.dropTgt) { 85 var args = {container:this._parentEl, threshold:15, amount:5, interval:10, id:params.id}; 86 this._dndScrollCallback = new AjxCallback(null, DwtControl._dndScrollCallback, [args]); 87 this._dndScrollId = params.id; 88 } 89 90 this._isPageless = params.pageless; 91 if (this._isPageless) { 92 Dwt.setHandler(this._getScrollDiv(), DwtEvent.ONSCROLL, ZmListView.handleScroll); 93 } 94 this._state = {}; 95 }; 96 97 ZmListView.prototype = new DwtListView; 98 ZmListView.prototype.constructor = ZmListView; 99 ZmListView.prototype.isZmListView = true; 100 101 ZmListView.prototype.toString = 102 function() { 103 return "ZmListView"; 104 }; 105 106 107 // Consts 108 109 ZmListView.KEY_ID = "_keyId"; 110 111 // column widths 112 ZmListView.COL_WIDTH_ICON = 19; 113 ZmListView.COL_WIDTH_NARROW_ICON = 11; 114 115 // TD class for fields 116 ZmListView.FIELD_CLASS = {}; 117 ZmListView.FIELD_CLASS[ZmItem.F_TYPE] = "ListViewIcon"; 118 ZmListView.FIELD_CLASS[ZmItem.F_FLAG] = "Flag"; 119 ZmListView.FIELD_CLASS[ZmItem.F_TAG] = "Tag"; 120 ZmListView.FIELD_CLASS[ZmItem.F_ATTACHMENT] = "Attach"; 121 122 ZmListView.ITEM_FLAG_CLICKED = DwtListView._LAST_REASON + 1; 123 ZmListView.DEFAULT_REPLENISH_THRESHOLD = 0; 124 125 ZmListView.COL_JOIN = "|"; 126 127 ZmListView.CHECKED_IMAGE = "CheckboxChecked"; 128 ZmListView.UNCHECKED_IMAGE = "CheckboxUnchecked"; 129 ZmListView.CHECKED_CLASS = "ImgCheckboxChecked"; 130 ZmListView.UNCHECKED_CLASS = "ImgCheckboxUnchecked"; 131 ZmListView.ITEM_CHECKED_ATT_NAME = "itemChecked"; 132 133 134 ZmListView.prototype._getHeaderList = function() {}; 135 136 /** 137 * Gets the controller. 138 * 139 * @return {ZmListController} the list controller 140 */ 141 ZmListView.prototype.getController = 142 function() { 143 return this._controller; 144 }; 145 146 ZmListView.prototype.set = 147 function(list, sortField) { 148 149 this._sortByString = this._controller._currentSearch && this._controller._currentSearch.sortBy; 150 //TODO: We need a longer term fix but this is to prevent a sort by that doesn't match our ZmSearch 151 //constants and lead to notification issues. 152 if (this._sortByString) { 153 this._sortByString = this._sortByString.replace("asc", "Asc").replace("desc", "Desc");// bug 75687 154 } 155 156 var settings = appCtxt.getSettings(); 157 if (!appCtxt.isExternalAccount() && this.view) { 158 appCtxt.set(ZmSetting.SORTING_PREF, 159 this._sortByString, 160 this.view, 161 false, //setDefault 162 false, //skipNotify 163 null, //account 164 settings && !settings.persistImplicitSortPrefs(this.view)); //skipImplicit - do not persist 165 } 166 167 this.setSelectionHdrCbox(false); 168 169 // bug fix #28595 - in multi-account, reset tag list change listeners 170 if (appCtxt.multiAccounts) { 171 var tagList = appCtxt.getTagTree(); 172 if (tagList) { 173 tagList.addChangeListener(this._tagListChangeListener); 174 } 175 } 176 177 if (this._isPageless) { 178 if (this._itemsToAdd) { 179 if (this._itemsToAdd.length) { 180 this.addItems(this._itemsToAdd); 181 this._itemsToAdd = null; 182 } 183 } else { 184 var lvList = list; 185 if (list && list.isZmList) { 186 list.addChangeListener(this._listChangeListener); 187 lvList = list.getSubList(0, list.size()); 188 } 189 DwtListView.prototype.set.call(this, lvList, sortField); 190 } 191 this._setRowHeight(); 192 } else { 193 var subList; 194 if (list && list.isZmList) { 195 list.addChangeListener(this._listChangeListener); 196 subList = list.getSubList(this.offset, this.getLimit()); 197 } else { 198 subList = list; 199 } 200 DwtListView.prototype.set.call(this, subList, sortField); 201 } 202 this._rendered = true; 203 204 // check in case there are more items but no scrollbar 205 if (this._isPageless) { 206 AjxTimedAction.scheduleAction(new AjxTimedAction(this, this._checkItemCount), 1000); 207 } 208 }; 209 210 ZmListView.prototype.reset = 211 function() { 212 this._rendered = false; 213 }; 214 215 ZmListView.prototype.setUI = 216 function(defaultColumnSort) { 217 DwtListView.prototype.setUI.call(this, defaultColumnSort); 218 this._resetColWidth(); // reset column width in case scrollbar is set 219 }; 220 221 /** 222 * Gets the limit value. 223 * 224 * @param {Boolean} offset if <code>true</code>, offset 225 * @return {int} the limit page size 226 */ 227 ZmListView.prototype.getLimit = 228 function(offset) { 229 if (this._isPageless) { 230 var limit = appCtxt.get(ZmSetting.PAGE_SIZE); 231 return offset ? limit : 2 * limit; 232 } else { 233 return appCtxt.get(ZmSetting.PAGE_SIZE); 234 } 235 }; 236 237 /** 238 * Gets the pageless threshold. 239 * 240 * @return {int} the pageless threshold 241 */ 242 ZmListView.prototype.getPagelessThreshold = 243 function() { 244 return Math.ceil(this.getLimit() / 5); 245 }; 246 247 /** 248 * Gets the replenish threshold. 249 * 250 * @return {int} the replenish threshold 251 */ 252 ZmListView.prototype.getReplenishThreshold = 253 function() { 254 return ZmListView.DEFAULT_REPLENISH_THRESHOLD; 255 }; 256 257 /** 258 * Returns the underlying ZmList. 259 */ 260 ZmListView.prototype.getItemList = 261 function() { 262 return this._controller && this._controller._list; 263 }; 264 265 ZmListView.prototype._changeListener = 266 function(ev) { 267 268 var item = this._getItemFromEvent(ev); 269 if (!item || ev.handled || !this._handleEventType[item.type]) { 270 return; 271 } 272 273 if (ev.event === ZmEvent.E_TAGS || ev.event === ZmEvent.E_REMOVE_ALL) { 274 this._replaceTagImage(item, ZmItem.F_TAG, this._getClasses(ZmItem.F_TAG)); 275 } 276 277 if (ev.event === ZmEvent.E_FLAGS) { 278 var flags = ev.getDetail("flags"); 279 for (var j = 0; j < flags.length; j++) { 280 var flag = flags[j]; 281 var on = item[ZmItem.FLAG_PROP[flag]]; 282 if (flag === ZmItem.FLAG_FLAGGED) { 283 this._setImage(item, ZmItem.F_FLAG, on ? "FlagRed" : "FlagDis", this._getClasses(ZmItem.F_FLAG)); 284 } 285 else if (flag === ZmItem.FLAG_ATTACH) { 286 this._setImage(item, ZmItem.F_ATTACHMENT, on ? "Attachment" : null, this._getClasses(ZmItem.F_ATTACHMENT)); 287 } 288 else if (flag === ZmItem.FLAG_PRIORITY) { 289 this._setImage(item, ZmItem.F_MSG_PRIORITY, on ? "Priority" : "PriorityDis", this._getClasses(ZmItem.F_MSG_PRIORITY)); 290 } 291 } 292 } 293 294 // Note: move and delete support batch notification mode 295 if (ev.event === ZmEvent.E_DELETE || ev.event === ZmEvent.E_MOVE) { 296 var items = ev.batchMode ? this._getItemsFromBatchEvent(ev) : [item]; 297 var needsSort = false; 298 for (var i = 0, len = items.length; i < len; i++) { 299 var item = items[i]; 300 var movedHere = (item.type === ZmId.ITEM_CONV) ? item.folders[this._folderId] : item.folderId === this._folderId; 301 if (movedHere && ev.event === ZmEvent.E_MOVE) { 302 // We've moved the item into this folder 303 if (this._getRowIndex(item) === null) { // Not already here 304 this.addItem(item); 305 // TODO: couldn't we just find the sort index and insert it? 306 needsSort = true; 307 } 308 } 309 else { 310 // remove the item if the user is working in this view, 311 // if we know the item no longer matches the search, or if the item was hard-deleted 312 if (ev.event === ZmEvent.E_DELETE || this.view == appCtxt.getCurrentViewId() || this._controller._currentSearch.matches(item) === false) { 313 this.removeItem(item, true, ev.batchMode); 314 // if we've removed it from the view, we should remove it from the reference 315 // list as well so it doesn't get resurrected via replenishment *unless* 316 // we're dealing with a canonical list (i.e. contacts) 317 var itemList = this.getItemList(); 318 if (ev.event !== ZmEvent.E_MOVE || !itemList.isCanonical) { 319 itemList.remove(item); 320 } 321 } 322 } 323 } 324 if (needsSort) { 325 this._saveState({scroll: true, selection:true, focus: true}); 326 this._redoSearch(this._restoreState.bind(this, this._state)); 327 } 328 if (ev.batchMode) { 329 this._fixAlternation(0); 330 } 331 this._checkReplenishOnTimer(); 332 this._controller._resetToolbarOperations(); 333 } 334 335 this._updateLabelForItem(item); 336 }; 337 338 ZmListView.prototype._getItemFromEvent = 339 function(ev) { 340 var item = ev.item; 341 if (!item) { 342 var items = ev.getDetail("items"); 343 item = (items && items.length) ? items[0] : null; 344 } 345 return item; 346 }; 347 348 ZmListView.prototype._getItemsFromBatchEvent = 349 function(ev) { 350 351 if (!ev.batchMode) { return []; } 352 353 var items = ev.items; 354 if (!items) { 355 items = []; 356 var notifs = ev.getDetail("notifs"); 357 if (notifs && notifs.length) { 358 for (var i = 0, len = notifs.length; i < len; i++) { 359 var mod = notifs[i]; 360 items.push(mod.item || appCtxt.cacheGet(mod.id)); 361 } 362 } 363 } 364 365 return items; 366 }; 367 368 // refreshes the content of the given field for the given item 369 ZmListView.prototype._updateField = 370 function(item, field) { 371 var fieldId = this._getFieldId(item, field); 372 var el = document.getElementById(fieldId); 373 if (el) { 374 var html = []; 375 var colIdx = this._headerHash[field] && this._headerHash[field]._index; 376 this._getCellContents(html, 0, item, field, colIdx, new Date()); 377 //replace the old inner html with the new updated data 378 el.innerHTML = $(html.join("")).html(); 379 } 380 381 this._updateLabelForItem(item); 382 }; 383 384 ZmListView.prototype._checkReplenishOnTimer = 385 function(ev) { 386 if (!this.allSelected) { 387 if (!this._isPageless) { 388 this._controller._app._checkReplenishListView = this; 389 } else { 390 // Many rows may be removed quickly, so skip unnecessary replenishes 391 if (!this._replenishTimedAction) { 392 this._replenishTimedAction = new AjxTimedAction(this, this._handleResponseCheckReplenish); 393 } 394 AjxTimedAction.scheduleAction(this._replenishTimedAction, 10); 395 } 396 } 397 }; 398 399 ZmListView.prototype._checkReplenish = 400 function(item, forceSelection) { 401 var respCallback = new AjxCallback(this, this._handleResponseCheckReplenish, [false, item, forceSelection]); 402 this._controller._checkReplenish(respCallback); 403 }; 404 405 ZmListView.prototype._handleResponseCheckReplenish = 406 function(skipSelection, item, forceSelection) { 407 if (this.size() == 0) { 408 this._controller._handleEmptyList(this); 409 } else { 410 this._controller._resetNavToolBarButtons(); 411 } 412 if (!skipSelection) { 413 this._setNextSelection(item, forceSelection); 414 } 415 }; 416 417 ZmListView.prototype._folderChangeListener = 418 function(ev) { 419 // make sure this is current list view 420 if (appCtxt.getCurrentController() != this._controller) { return; } 421 // see if it will be handled by app's postNotify() 422 if (this._controller._app._checkReplenishListView == this) { return; } 423 424 var organizers = ev.getDetail("organizers"); 425 var organizer = (organizers && organizers.length) ? organizers[0] : ev.source; 426 427 var id = organizer.id; 428 var fields = ev.getDetail("fields"); 429 if (ev.event == ZmEvent.E_MODIFY) { 430 if (!fields) { return; } 431 if (fields[ZmOrganizer.F_TOTAL]) { 432 this._controller._resetNavToolBarButtons(); 433 } 434 } 435 }; 436 437 ZmListView.prototype._tagChangeListener = 438 function(ev) { 439 if (ev.type != ZmEvent.S_TAG) return; 440 441 var fields = ev.getDetail("fields"); 442 443 var divs = this._getChildren(); 444 var tag = ev.getDetail("organizers")[0]; 445 for (var i = 0; i < divs.length; i++) { 446 var item = this.getItemFromElement(divs[i]); 447 if (!item || !item.tags || !item.hasTag(tag.name)) { 448 continue; 449 } 450 var updateRequired = false; 451 if (ev.event == ZmEvent.E_MODIFY && (fields && (fields[ZmOrganizer.F_COLOR] || fields[ZmOrganizer.F_NAME]))) { 452 //rename could change the color (for remote shared items, from the remote gray icon to local color and vice versa) 453 updateRequired = item.tags.length == 1; 454 } 455 else if (ev.event == ZmEvent.E_DELETE) { 456 updateRequired = true; 457 } 458 else if (ev.event == ZmEvent.E_CREATE) { 459 //this could affect item if it had a tag not on tag list (remotely created on shared item, either shared by this user or shared to this user) 460 updateRequired = true; 461 } 462 if (updateRequired) { 463 this._replaceTagImage(item, ZmItem.F_TAG, this._getClasses(ZmItem.F_TAG)); 464 } 465 } 466 }; 467 468 // returns all child divs for this list view 469 ZmListView.prototype._getChildren = 470 function() { 471 return this._parentEl.childNodes; 472 }; 473 474 // Common routines for createItemHtml() 475 476 ZmListView.prototype._getRowId = 477 function(item) { 478 return DwtId.getListViewItemId(DwtId.WIDGET_ITEM_FIELD, this._view, item ? item.id : Dwt.getNextId(), ZmItem.F_ITEM_ROW); 479 }; 480 481 // Note that images typically get IDs in _getCellContents(). 482 ZmListView.prototype._getCellId = 483 function(item, field) { 484 if (field == ZmItem.F_DATE) { 485 return this._getFieldId(item, field); 486 } else if (field == ZmItem.F_SELECTION) { 487 return this._getFieldId(item, ZmItem.F_SELECTION_CELL); 488 489 } else { 490 return DwtListView.prototype._getCellId.apply(this, arguments); 491 } 492 }; 493 494 ZmListView.prototype._getCellClass = 495 function(item, field, params) { 496 return ZmListView.FIELD_CLASS[field]; 497 }; 498 499 ZmListView.prototype._getCellContents = 500 function(htmlArr, idx, item, field, colIdx, params, classes) { 501 if (field == ZmItem.F_SELECTION) { 502 idx = this._getImageHtml(htmlArr, idx, "CheckboxUnchecked", this._getFieldId(item, field), classes); 503 } else if (field == ZmItem.F_TYPE) { 504 idx = this._getImageHtml(htmlArr, idx, ZmItem.ICON[item.type], this._getFieldId(item, field), classes); 505 } else if (field == ZmItem.F_FLAG) { 506 idx = this._getImageHtml(htmlArr, idx, this._getFlagIcon(item.isFlagged), this._getFieldId(item, field), classes); 507 } else if (field == ZmItem.F_TAG) { 508 idx = this._getImageHtml(htmlArr, idx, item.getTagImageInfo(), this._getFieldId(item, field), classes); 509 } else if (field == ZmItem.F_ATTACHMENT) { 510 idx = this._getImageHtml(htmlArr, idx, item.hasAttach ? "Attachment" : null, this._getFieldId(item, field), classes); 511 } else if (field == ZmItem.F_DATE) { 512 htmlArr[idx++] = AjxDateUtil.computeDateStr(params.now || new Date(), item.date); 513 } else if (field == ZmItem.F_PRIORITY) { 514 var priorityImage = null; 515 if (item.isHighPriority) { 516 priorityImage = "PriorityHigh_list"; 517 } else if (item.isLowPriority) { 518 priorityImage = "PriorityLow_list"; 519 } 520 if (priorityImage) { 521 idx = this._getImageHtml(htmlArr, idx, priorityImage, this._getFieldId(item, field), classes); 522 } else { 523 htmlArr[idx++] = "<div id='" + this._getFieldId(item, field) + "' " + AjxUtil.getClassAttr(classes) + "></div>"; 524 } 525 } else { 526 idx = DwtListView.prototype._getCellContents.apply(this, arguments); 527 } 528 return idx; 529 }; 530 531 ZmListView.prototype._getImageHtml = 532 function(htmlArr, idx, imageInfo, id, classes) { 533 htmlArr[idx++] = "<div"; 534 if (id) { 535 htmlArr[idx++] = [" id='", id, "' "].join(""); 536 } 537 htmlArr[idx++] = AjxUtil.getClassAttr(classes); 538 htmlArr[idx++] = ">"; 539 htmlArr[idx++] = AjxImg.getImageHtml(imageInfo || "Blank_16"); 540 htmlArr[idx++] = "</div>"; 541 return idx; 542 }; 543 544 ZmListView.prototype._getClasses = 545 function(field, classes) { 546 if (this.isMultiColumn && this.isMultiColumn() && this._headerHash[field]) { 547 classes = classes || []; 548 classes = [this._headerHash[field]._cssClass]; 549 } 550 return classes; 551 }; 552 553 ZmListView.prototype._setImage = 554 function(item, field, imageInfo, classes) { 555 var cell = this._getElement(item, field); 556 if (cell) { 557 if (classes) { 558 cell.className = AjxUtil.uniq(classes).join(" "); 559 } 560 cell.innerHTML = AjxImg.getImageHtml(imageInfo || "Blank_16"); 561 } 562 }; 563 564 ZmListView.prototype._replaceTagImage = 565 function(item, field, classes) { 566 this._setImage(item, field, item.getTagImageInfo(), classes); 567 }; 568 569 ZmListView.prototype._getFragmentSpan = 570 function(item) { 571 return ["<span class='ZmConvListFragment' aria-hidden='true' id='", 572 this._getFieldId(item, ZmItem.F_FRAGMENT), 573 "'>", this._getFragmentHtml(item), "</span>"].join(""); 574 }; 575 576 ZmListView.prototype._getFragmentHtml = 577 function(item) { 578 return [" - ", AjxStringUtil.htmlEncode(item.fragment, true)].join(""); 579 }; 580 581 ZmListView.prototype._getFlagIcon = 582 function(isFlagged, isMouseover, disabled) { 583 if (!isFlagged && !isMouseover) { 584 return "Blank_16"; 585 } else if (disabled) { 586 return "FlagDis"; 587 } else { 588 return "FlagRed"; 589 } 590 }; 591 592 /** 593 * Parse the DOM ID to figure out what got clicked. IDs consist of three to five parts 594 * joined by the "|" character. 595 * 596 * type type of ID (zli, zlir, zlic, zlif) - see DwtId.WIDGET_ITEM*) 597 * view view identifier (eg "TV") 598 * item ID usually numeric 599 * field field identifier (eg "fg") - see ZmId.FLG_* 600 * participant index of participant 601 */ 602 ZmListView.prototype._parseId = 603 function(id) { 604 var parts = id.split(DwtId.SEP); 605 if (parts && parts.length) { 606 return {view:parts[1], item:parts[2], field:parts[3], participant:parts[4]}; 607 } else { 608 return null; 609 } 610 }; 611 612 ZmListView.prototype._mouseDownAction = 613 function(ev, div) { 614 return !Dwt.ffScrollbarCheck(ev); 615 }; 616 617 ZmListView.prototype._mouseUpAction = 618 function(ev, div) { 619 return !Dwt.ffScrollbarCheck(ev); 620 }; 621 622 ZmListView.prototype._getField = 623 function(ev, div) { 624 625 var target = this._getEventTarget(ev); 626 627 var id = target && target.id || div.id; 628 if (!id) { 629 return null; 630 } 631 632 var data = this._data[div.id]; 633 var type = data.type; 634 if (!type || type != DwtListView.TYPE_LIST_ITEM) { 635 return null; 636 } 637 638 var m = this._parseId(id); 639 if (!m || !m.field) { 640 return null; 641 } 642 return m.field; 643 644 }; 645 646 647 ZmListView.prototype._mouseOutAction = 648 function(ev, div) { 649 DwtListView.prototype._mouseOutAction.call(this, ev, div); 650 651 var field = this._getField(ev, div); 652 if (!field) { 653 return true; 654 } 655 656 if (field == ZmItem.F_FLAG) { 657 var item = this.getItemFromElement(div); 658 if (!item.isFlagged) { 659 var target = this._getEventTarget(ev); 660 AjxImg.setImage(target, this._getFlagIcon(item.isFlagged, false), false, false); 661 target.className = this._getClasses(field); 662 } 663 } 664 return true; 665 }; 666 667 668 ZmListView.prototype._mouseOverAction = 669 function(ev, div) { 670 DwtListView.prototype._mouseOverAction.call(this, ev, div); 671 672 var field = this._getField(ev, div); 673 if (!field) { 674 return true; 675 } 676 677 if (field === ZmItem.F_FLAG) { 678 var item = this.getItemFromElement(div); 679 if (!item.isReadOnly() && !item.isFlagged) { 680 var target = this._getEventTarget(ev); 681 AjxImg.setDisabledImage(target, this._getFlagIcon(item.isFlagged, true), false); 682 target.className = this._getClasses(field); 683 } 684 } 685 return true; 686 }; 687 688 689 690 ZmListView.prototype._doubleClickAction = 691 function(ev, div) { 692 var target = this._getEventTarget(ev); 693 var id = target && target.id || div.id; 694 if (!id) { return true; } 695 696 var m = this._parseId(id); 697 return (!(m && (m.field == ZmItem.F_FLAG))); 698 }; 699 700 ZmListView.prototype._itemClicked = 701 function(clickedEl, ev) { 702 if (appCtxt.get(ZmSetting.SHOW_SELECTION_CHECKBOX) && ev.button == DwtMouseEvent.LEFT) { 703 if (!ev.shiftKey && !ev.ctrlKey) { 704 // get the field being clicked 705 var target = this._getEventTarget(ev); 706 var id = (target && target.id && target.id.indexOf("AjxImg") == -1) ? target.id : clickedEl.id; 707 var m = id ? this._parseId(id) : null; 708 if (m && (m.field == ZmItem.F_SELECTION || m.field == ZmItem.F_SELECTION_CELL)) { 709 //user clicked on a checkbox 710 if (this._selectedItems.size() == 1) { 711 var sel = this._selectedItems.get(0); 712 var item = this.getItemFromElement(sel); 713 var selFieldId = item ? this._getFieldId(item, ZmItem.F_SELECTION) : null; 714 var selField = selFieldId ? document.getElementById(selFieldId) : null; 715 if (selField && sel == clickedEl) { 716 var isChecked = this._getItemData(sel, ZmListView.ITEM_CHECKED_ATT_NAME); 717 this._setImage(item, ZmItem.F_SELECTION, isChecked ? ZmListView.UNCHECKED_IMAGE : ZmListView.CHECKED_IMAGE); 718 this._setItemData(sel, ZmListView.ITEM_CHECKED_ATT_NAME, !isChecked); 719 if (!isChecked) { 720 return; //nothing else to do. It's already selected, and was the only selected one. Nothing to remove 721 } 722 } else { 723 if (selField && !this._getItemData(sel, ZmListView.ITEM_CHECKED_ATT_NAME)) { 724 this.deselectAll(); 725 this._markUnselectedViewedItem(true); 726 } 727 } 728 } 729 var bContained = this._selectedItems.contains(clickedEl); 730 this.setMultiSelection(clickedEl, bContained); 731 this._controller._setItemSelectionCountText(); 732 return; // do not call base class if "selection" field was clicked 733 } 734 } else if (ev.shiftKey) { 735 // uncheck all selected items first 736 this._checkSelectedItems(false); 737 738 // run base class first so we get the finalized list of selected items 739 DwtListView.prototype._itemClicked.call(this, clickedEl, ev); 740 741 // recheck new list of selected items 742 this._checkSelectedItems(true); 743 744 return; 745 } 746 } 747 748 DwtListView.prototype._itemClicked.call(this, clickedEl, ev); 749 }; 750 751 ZmListView.prototype._columnClicked = 752 function(clickedCol, ev) { 753 DwtListView.prototype._columnClicked.call(this, clickedCol, ev); 754 this._checkSelectionColumnClicked(clickedCol, ev); 755 }; 756 757 ZmListView.prototype._checkSelectionColumnClicked = 758 function(clickedCol, ev) { 759 760 if (!appCtxt.get(ZmSetting.SHOW_SELECTION_CHECKBOX)) { return; } 761 762 var list = this.getList(); 763 var size = list ? list.size() : null; 764 if (size > 0) { 765 var idx = this._data[clickedCol.id].index; 766 var item = this._headerList[idx]; 767 if (item && (item._field == ZmItem.F_SELECTION)) { 768 var hdrId = DwtId.getListViewHdrId(DwtId.WIDGET_HDR_ICON, this._view, item._field); 769 var hdrDiv = document.getElementById(hdrId); 770 if (hdrDiv) { 771 if (hdrDiv.className == ZmListView.CHECKED_CLASS) { 772 if (ev.shiftKey && !this.allSelected) { 773 this.selectAll(ev.shiftKey); 774 } else { 775 this.deselectAll(); 776 hdrDiv.className = ZmListView.UNCHECKED_CLASS; 777 } 778 } else { 779 this.allSelected = false; 780 hdrDiv.className = ZmListView.CHECKED_CLASS; 781 this.selectAll(ev.shiftKey); 782 } 783 } 784 } 785 this._controller._resetToolbarOperations(); 786 } 787 }; 788 789 ZmListView.prototype.handleKeyAction = 790 function(actionCode, ev) { 791 var rv = DwtListView.prototype.handleKeyAction.call(this, actionCode, ev); 792 793 if (actionCode == DwtKeyMap.SELECT_ALL) { 794 this._controller._resetToolbarOperations(); 795 } 796 797 return rv; 798 }; 799 800 ZmListView.prototype.setMultiSelection = 801 function(clickedEl, bContained, ev) { 802 if (ev && ev.ctrlKey && this._selectedItems.size() == 1) { 803 this._checkSelectedItems(true); 804 } 805 806 // call base class 807 DwtListView.prototype.setMultiSelection.call(this, clickedEl, bContained); 808 809 this.setSelectionCbox(clickedEl, bContained); 810 this.setSelectionHdrCbox(this._isAllChecked()); 811 812 // reset toolbar operations LAST 813 this._controller._resetToolbarOperations(); 814 }; 815 816 /** 817 * check whether all items in the list are checked 818 * @return {Boolean} true if all items are checked 819 */ 820 ZmListView.prototype._isAllChecked = 821 function() { 822 var list = this.getList(); 823 return (list && (this.getSelection().length == list.size())); 824 }; 825 826 827 /** 828 * Sets the selection checkbox. 829 * 830 * @param {Element} obj the item element object 831 * @param {Boolean} bContained (not used) 832 * 833 */ 834 ZmListView.prototype.setSelectionCbox = 835 function(obj, bContained) { 836 if (!obj) { return; } 837 838 var item = obj.tagName ? this.getItemFromElement(obj) : obj; 839 var selFieldId = item ? this._getFieldId(item, ZmItem.F_SELECTION) : null; 840 var selField = selFieldId ? document.getElementById(selFieldId) : null; 841 if (selField) { 842 this._setImage(item, ZmItem.F_SELECTION, bContained ? ZmListView.UNCHECKED_IMAGE : ZmListView.CHECKED_IMAGE); 843 this._setItemData(this._getElFromItem(item), ZmListView.ITEM_CHECKED_ATT_NAME, !bContained); 844 } 845 }; 846 847 /** 848 * Sets the selection header checkbox. 849 * 850 * @param {Boolean} check if <code>true</code>, check the header checkbox 851 */ 852 ZmListView.prototype.setSelectionHdrCbox = 853 function(check) { 854 var col = this._headerHash ? this._headerHash[ZmItem.F_SELECTION] : null; 855 var hdrId = col ? DwtId.getListViewHdrId(DwtId.WIDGET_HDR_ICON, this._view, col._field) : null; 856 var hdrDiv = hdrId ? document.getElementById(hdrId) : null; 857 if (hdrDiv) { 858 hdrDiv.className = check 859 ? ZmListView.CHECKED_CLASS 860 : ZmListView.UNCHECKED_CLASS; 861 } 862 }; 863 864 /** 865 * Sets the selected items. 866 * 867 * @param {Array} selectedArray an array of {Element} objects to select 868 * @param {boolean} dontCheck do not check the selected item. (special case. see ZmListView.prototype._restoreState) 869 */ 870 ZmListView.prototype.setSelectedItems = 871 function(selectedArray, dontCheck) { 872 DwtListView.prototype.setSelectedItems.call(this, selectedArray); 873 874 if (!dontCheck && appCtxt.get(ZmSetting.SHOW_SELECTION_CHECKBOX)) { 875 this._checkSelectedItems(true, true); 876 } 877 }; 878 879 /** 880 * Selects all items. 881 * 882 * @param {Boolean} allResults if <code>true</code>, set all search selected 883 */ 884 ZmListView.prototype.selectAll = 885 function(allResults) { 886 887 DwtListView.prototype.selectAll.apply(this, arguments); 888 889 if (this._selectAllEnabled) { 890 var curResult = this._controller._activeSearch; 891 if (curResult && curResult.getAttribute("more")) { 892 893 var list = this.getList(), 894 type = this.type, 895 countKey = 'type' + AjxStringUtil.capitalize(ZmItem.MSG_KEY[type]), 896 typeText = AjxMessageFormat.format(ZmMsg[countKey], list ? list.size() : 2), 897 shortcut = appCtxt.getShortcutHint(null, ZmKeyMap.SELECT_ALL), 898 args = [list ? list.size() : ZmMsg.all, typeText, shortcut, "ZmListView.selectAllResults()"], 899 toastMsg = AjxMessageFormat.format(ZmMsg.allPageSelected, args); 900 901 if (allResults) { 902 this.allSelected = true; 903 toastMsg = ZmMsg.allSearchSelected; 904 } 905 appCtxt.setStatusMsg(toastMsg); 906 } 907 908 var sel = this._selectedItems.getArray(); 909 for (var i = 0; i < sel.length; i++) { 910 this.setSelectionCbox(sel[i], false); 911 } 912 } 913 }; 914 915 // Handle click of link in toast 916 ZmListView.selectAllResults = 917 function() { 918 var ctlr = appCtxt.getCurrentController(); 919 var view = ctlr && ctlr.getListView(); 920 if (view && view.selectAll) { 921 view.selectAll(true); 922 } 923 }; 924 925 /** 926 * Deselects all items. 927 * 928 */ 929 ZmListView.prototype.deselectAll = 930 function() { 931 932 this.allSelected = false; 933 if (appCtxt.get(ZmSetting.SHOW_SELECTION_CHECKBOX)) { 934 this._checkSelectedItems(false); 935 var hdrId = DwtId.getListViewHdrId(DwtId.WIDGET_HDR_ICON, this._view, ZmItem.F_SELECTION); 936 var hdrDiv = document.getElementById(hdrId); 937 if (hdrDiv) { 938 hdrDiv.className = ZmListView.UNCHECKED_CLASS; 939 } 940 var sel = this._selectedItems.getArray(); 941 for (var i=0; i<sel.length; i++) { 942 this.setSelectionCbox(sel[i], true); 943 } 944 } 945 946 DwtListView.prototype.deselectAll.call(this); 947 }; 948 949 ZmListView.prototype._checkSelectedItems = 950 function(check) { 951 var sel = this.getSelection(); 952 for (var i = 0; i < sel.length; i++) { 953 this.setSelectionCbox(sel[i], !check); 954 } 955 956 var list = this.getList(); 957 var size = list && list.size(); 958 this.setSelectionHdrCbox(size && sel.length == size); 959 }; 960 961 ZmListView.prototype._setNoResultsHtml = 962 function() { 963 DwtListView.prototype._setNoResultsHtml.call(this); 964 this.setSelectionHdrCbox(false); 965 this._rendered = true; 966 }; 967 968 /** 969 * override to call _resetToolbarOperations since we change the selection. 970 * @private 971 */ 972 ZmListView.prototype._clearRightSel = 973 function() { 974 DwtListView.prototype._clearRightSel.call(this); 975 this._controller._resetToolbarOperations(); 976 }; 977 978 979 /* 980 get sort menu for views that provide a right-click sort by menu in single-column view (currently mail and briefcase) 981 */ 982 ZmListView.prototype._getSortMenu = function (sortFields, defaultSortField, parent) { 983 984 // create an action menu for the header list 985 var menu = new ZmPopupMenu(parent || this, null, Dwt.getNextId("SORT_MENU_")); 986 var actionListener = this._sortMenuListener.bind(this); 987 988 for (var i = 0; i < sortFields.length; i++) { 989 var column = sortFields[i]; 990 var fieldName = ZmMsg[column.msg]; 991 var mi = menu.createMenuItem(column.field, { 992 text: parent && parent.isDwtMenuItem ? fieldName : AjxMessageFormat.format(ZmMsg.arrangeBy, fieldName), 993 style: DwtMenuItem.RADIO_STYLE 994 }); 995 if (column.field == defaultSortField) { 996 mi.setChecked(true, true); 997 } 998 mi.setData(ZmListView.KEY_ID, column.field); 999 menu.addSelectionListener(column.field, actionListener); 1000 } 1001 1002 return menu; 1003 }; 1004 1005 /* 1006 listener used by views that provide a right-click sort by menu in single-column view (currently mail and briefcase) 1007 */ 1008 ZmListView.prototype._sortMenuListener = 1009 function(ev) { 1010 var column; 1011 if (this.isMultiColumn()) { //this can happen when called from the view menu, that now, for accessibility reasons, includes the sort, for both reading pane on right and at the bottom. 1012 var sortField = ev && ev.item && ev.item.getData(ZmOperation.MENUITEM_ID); 1013 column = this._headerHash[sortField]; 1014 } 1015 else { 1016 column = this._headerHash[ZmItem.F_SORTED_BY]; 1017 var cell = document.getElementById(DwtId.getListViewHdrId(DwtId.WIDGET_HDR_LABEL, this._view, column._field)); 1018 if (cell) { 1019 var text = ev.item.getText(); 1020 cell.innerHTML = text && text.replace(ZmMsg.sortBy, ZmMsg.sortedBy); 1021 } 1022 column._sortable = ev.item.getData(ZmListView.KEY_ID); 1023 } 1024 this._bSortAsc = (column._sortable === this._currentSortColId) ? !this._bSortAsc : this._isDefaultSortAscending(column); 1025 this._sortColumn(column, this._bSortAsc); 1026 }; 1027 1028 ZmListView.prototype._getActionMenuForColHeader = function(force, parent, context) { 1029 1030 var menu; 1031 if (!this._colHeaderActionMenu || force) { 1032 // create an action menu for the header list 1033 menu = new ZmPopupMenu(parent || this); 1034 var actionListener = this._colHeaderActionListener.bind(this); 1035 for (var i = 0; i < this._headerList.length; i++) { 1036 var hCol = this._headerList[i]; 1037 // lets not allow columns w/ relative width to be removed (for now) - it messes stuff up 1038 if (hCol._width) { 1039 var id = ZmId.getMenuItemId([ this._view, context ].join("_"), hCol._field); 1040 var mi = menu.createMenuItem(id, {text:hCol._name, style:DwtMenuItem.CHECK_STYLE}); 1041 mi.setData(ZmListView.KEY_ID, hCol._id); 1042 mi.setChecked(hCol._visible, true); 1043 if (hCol._noRemove) { 1044 mi.setEnabled(false); 1045 } 1046 menu.addSelectionListener(id, actionListener); 1047 } 1048 } 1049 } 1050 1051 return menu; 1052 }; 1053 1054 ZmListView.prototype._colHeaderActionListener = 1055 function(ev) { 1056 1057 var menuItemId = ev.item.getData(ZmListView.KEY_ID); 1058 1059 for (var i = 0; i < this._headerList.length; i++) { 1060 var col = this._headerList[i]; 1061 if (col._id == menuItemId) { 1062 col._visible = !col._visible; 1063 break; 1064 } 1065 } 1066 1067 this._relayout(); 1068 }; 1069 1070 /** 1071 * Gets the tool tip content. 1072 * 1073 * @param {Object} ev the hover event 1074 * @return {String} the tool tip content 1075 */ 1076 ZmListView.prototype.getToolTipContent = function(ev) { 1077 1078 var div = this.getTargetItemDiv(ev); 1079 if (!div) { 1080 return ""; 1081 } 1082 var target = Dwt.findAncestor(this._getEventTarget(ev), "id"), 1083 id = (target && target.id) || div.id; 1084 1085 if (!id) { 1086 return ""; 1087 } 1088 1089 // check if we're hovering over a column header 1090 var data = this._data[div.id]; 1091 var type = data.type; 1092 var tooltip; 1093 if (type && type == DwtListView.TYPE_HEADER_ITEM) { 1094 var itemIdx = data.index; 1095 var field = this._headerList[itemIdx]._field; 1096 tooltip = this._getHeaderToolTip(field, itemIdx); 1097 } 1098 else { 1099 var match = this._parseId(id); 1100 if (match && match.field) { 1101 var item = this.getItemFromElement(div); 1102 var params = {field:match.field, item:item, ev:ev, div:div, match:match}; 1103 tooltip = this._getToolTip(params); 1104 } 1105 } 1106 1107 return tooltip; 1108 }; 1109 1110 ZmListView.prototype.getTooltipBase = 1111 function(hoverEv) { 1112 return hoverEv ? DwtUiEvent.getTargetWithProp(hoverEv.object, "id") : DwtListView.prototype.getTooltipBase.apply(this, arguments); 1113 }; 1114 1115 ZmListView.prototype._getHeaderToolTip = 1116 function(field, itemIdx, isOutboundFolder) { 1117 1118 var tooltip = null; 1119 var sortable = this._headerList[itemIdx]._sortable; 1120 if (field == ZmItem.F_SELECTION) { 1121 tooltip = ZmMsg.selectionColumn; 1122 } else if (field == ZmItem.F_FLAG) { 1123 tooltip = ZmMsg.flagHeaderToolTip; 1124 } else if (field == ZmItem.F_PRIORITY){ 1125 tooltip = ZmMsg.priorityHeaderTooltip; 1126 } else if (field == ZmItem.F_TAG) { 1127 tooltip = ZmMsg.tag; 1128 } else if (field == ZmItem.F_ATTACHMENT) { 1129 tooltip = ZmMsg.attachmentHeaderToolTip; 1130 } else if (field == ZmItem.F_SUBJECT) { 1131 tooltip = sortable ? ZmMsg.sortBySubject : ZmMsg.subject; 1132 } else if (field == ZmItem.F_DATE) { 1133 if (sortable) { 1134 if (isOutboundFolder) { 1135 tooltip = (this._folderId == ZmFolder.ID_DRAFTS) ? ZmMsg.sortByLastSaved : ZmMsg.sortBySent; 1136 } else { 1137 tooltip = ZmMsg.sortByReceived; 1138 } 1139 } else { 1140 tooltip = ZmMsg.date; 1141 } 1142 } else if (field == ZmItem.F_FROM) { 1143 tooltip = sortable ? isOutboundFolder ? ZmMsg.sortByTo : ZmMsg.sortByFrom : isOutboundFolder ? ZmMsg.to : ZmMsg.from ; 1144 } else if (field == ZmItem.F_SIZE){ 1145 tooltip = sortable ? ZmMsg.sortBySize : ZmMsg.sizeToolTip; 1146 } else if (field == ZmItem.F_ACCOUNT) { 1147 tooltip = ZmMsg.account; 1148 } else if (field == ZmItem.F_FOLDER) { 1149 tooltip = ZmMsg.folder; 1150 } else if (field == ZmItem.F_MSG_PRIORITY) { 1151 tooltip = ZmMsg.messagePriority 1152 } 1153 1154 return tooltip; 1155 }; 1156 1157 /** 1158 * @param params [hash] hash of params: 1159 * field [constant] column ID 1160 * item [ZmItem]* underlying item 1161 * ev [DwtEvent]* mouseover event 1162 * div [Element]* row div 1163 * match [hash]* fields from div ID 1164 * callback [AjxCallback]* callback (in case tooltip content retrieval is async) 1165 * 1166 * @private 1167 */ 1168 ZmListView.prototype._getToolTip = 1169 function(params) { 1170 var tooltip, field = params.field, item = params.item, div = params.div; 1171 if (field == ZmItem.F_FLAG) { 1172 return null; //no tooltip for the flag 1173 } else if (field == ZmItem.F_PRIORITY) { 1174 if (item.isHighPriority) { 1175 tooltip = ZmMsg.highPriorityTooltip; 1176 } else if (item.isLowPriority) { 1177 tooltip = ZmMsg.lowPriorityTooltip; 1178 } 1179 } else if (field == ZmItem.F_TAG) { 1180 tooltip = this._getTagToolTip(item); 1181 } else if (field == ZmItem.F_ATTACHMENT) { 1182 // disable att tooltip for now, we only get att info once msg is loaded 1183 // tooltip = this._getAttachmentToolTip(item); 1184 } else if (div && (field == ZmItem.F_DATE)) { 1185 tooltip = this._getDateToolTip(item, div); 1186 } 1187 return tooltip; 1188 }; 1189 1190 /* 1191 * Get the list of fields for the accessibility label. Normally, this 1192 * corresponds to the header columns. 1193 * 1194 * @protected 1195 */ 1196 ZmListView.prototype._getLabelFieldList = 1197 function() { 1198 var headers = this._getHeaderList(); 1199 1200 if (headers) { 1201 return AjxUtil.map(headers, function(header) { 1202 return header._field; 1203 }); 1204 } 1205 }; 1206 1207 /* 1208 * Get the accessibility label corresponding to the given field. 1209 * 1210 * @protected 1211 */ 1212 ZmListView.prototype._getLabelForField = 1213 function(item, field) { 1214 var tooltip = this._getToolTip({ item: item, field: field }); 1215 // TODO: fix for tooltips that are callbacks (such as for appts) 1216 return AjxStringUtil.stripTags(tooltip); 1217 }; 1218 1219 ZmListView.prototype._updateLabelForItem = 1220 function(item) { 1221 var fields = this._getLabelFieldList(); 1222 var itemel = this._getElFromItem(item); 1223 1224 if (!item || !fields || !itemel) { 1225 return; 1226 } 1227 1228 var buf = []; 1229 1230 for (var i = 0; i < fields.length; i++) { 1231 var label = this._getLabelForField(item, fields[i]); 1232 1233 if (label) { 1234 buf.push(label); 1235 } 1236 } 1237 1238 if (buf.length > 0) { 1239 itemel.setAttribute('aria-label', buf.join(', ')); 1240 } else { 1241 itemel.removeAttribute('aria-label'); 1242 } 1243 }; 1244 1245 ZmListView.prototype._getTagToolTip = 1246 function(item) { 1247 if (!item) { return; } 1248 var numTags = item.tags && item.tags.length; 1249 if (!numTags) { return; } 1250 var tagList = appCtxt.getAccountTagList(item); 1251 var tags = item.tags; 1252 var html = []; 1253 var idx = 0; 1254 for (var i = 0; i < numTags; i++) { 1255 var tag = tagList.getByNameOrRemote(tags[i]); 1256 if (!tag) { continue; } 1257 var nameText = tag.notLocal ? AjxMessageFormat.format(ZmMsg.tagNotLocal, tag.name) : tag.name; 1258 html[idx++] = "<table><tr><td>"; 1259 html[idx++] = AjxImg.getImageHtml(tag.getIconWithColor()); 1260 html[idx++] = "</td><td valign='middle'>"; 1261 html[idx++] = AjxStringUtil.htmlEncode(nameText); 1262 html[idx++] = "</td></tr></table>"; 1263 } 1264 return html.join(""); 1265 }; 1266 1267 ZmListView.prototype._getAttachmentToolTip = 1268 function(item) { 1269 var tooltip = null; 1270 var atts = item && item.attachments ? item.attachments : []; 1271 if (atts.length == 1) { 1272 var info = ZmMimeTable.getInfo(atts[0].ct); 1273 tooltip = info ? info.desc : null; 1274 } else if (atts.length > 1) { 1275 tooltip = AjxMessageFormat.format(ZmMsg.multipleAttachmentsTooltip, [atts.length]); 1276 } 1277 return tooltip; 1278 }; 1279 1280 ZmListView.prototype._getDateToolTip = 1281 function(item, div) { 1282 div._dateStr = div._dateStr || this._getDateToolTipText(item.date); 1283 return div._dateStr; 1284 }; 1285 1286 ZmListView.prototype._getDateToolTipText = 1287 function(date, prefix) { 1288 if (!date) { return ""; } 1289 var dateStr = []; 1290 var i = 0; 1291 dateStr[i++] = prefix; 1292 var dateFormatter = AjxDateFormat.getDateTimeInstance(AjxDateFormat.FULL, AjxDateFormat.MEDIUM); 1293 dateStr[i++] = dateFormatter.format(new Date(date)); 1294 var delta = AjxDateUtil.computeDateDelta(date); 1295 if (delta) { 1296 dateStr[i++] = "<br><center><span style='white-space:nowrap'>("; 1297 dateStr[i++] = delta; 1298 dateStr[i++] = ")</span></center>"; 1299 } 1300 return dateStr.join(""); 1301 }; 1302 1303 /* 1304 * Add a few properties to the list event for the listener to pick up. 1305 */ 1306 ZmListView.prototype._setListEvent = 1307 function (ev, listEv, clickedEl) { 1308 DwtListView.prototype._setListEvent.call(this, ev, listEv, clickedEl); 1309 var target = this._getEventTarget(ev); 1310 var id = (target && target.id && target.id.indexOf("AjxImg") == -1) ? target.id : clickedEl.id; 1311 if (!id) return false; // don't notify listeners 1312 1313 var m = this._parseId(id); 1314 if (ev.button == DwtMouseEvent.LEFT) { 1315 this._selEv.field = m ? m.field : null; 1316 } else if (ev.button == DwtMouseEvent.RIGHT) { 1317 this._actionEv.field = m ? m.field : null; 1318 if (m && m.field) { 1319 if (m.field == ZmItem.F_PARTICIPANT) { 1320 var item = this.getItemFromElement(clickedEl); 1321 this._actionEv.detail = item.participants ? item.participants.get(m.participant) : null; 1322 } 1323 } 1324 } 1325 return true; 1326 }; 1327 1328 ZmListView.prototype._allowLeftSelection = 1329 function(clickedEl, ev, button) { 1330 // We only care about mouse events 1331 if (!(ev instanceof DwtMouseEvent)) { return true; } 1332 var target = this._getEventTarget(ev); 1333 var id = (target && target.id && target.id.indexOf("AjxImg") == -1) ? target.id : clickedEl.id; 1334 var data = this._data[clickedEl.id]; 1335 var type = data.type; 1336 if (id && type && type == DwtListView.TYPE_LIST_ITEM) { 1337 var m = this._parseId(id); 1338 if (m && m.field) { 1339 return this._allowFieldSelection(m.item, m.field); 1340 } 1341 } 1342 return true; 1343 }; 1344 1345 ZmListView.prototype._allowFieldSelection = 1346 function(id, field) { 1347 return (!this._disallowSelection[field]); 1348 }; 1349 1350 ZmListView.prototype._redoSearch = 1351 function(callback) { 1352 var search = this._controller._currentSearch; 1353 if (!search) { 1354 return; 1355 } 1356 var sel = this.getSelection(); 1357 var selItem = sel && sel[0]; 1358 var changes = { 1359 isRedo: true, 1360 selectedItem: selItem 1361 }; 1362 appCtxt.getSearchController().redoSearch(search, false, changes, callback); 1363 }; 1364 1365 ZmListView.prototype._sortColumn = function(columnItem, bSortAsc, callback) { 1366 1367 // change the sort preference for this view in the settings 1368 var sortBy; 1369 switch (columnItem._sortable) { 1370 case ZmItem.F_FROM: sortBy = bSortAsc ? ZmSearch.NAME_ASC : ZmSearch.NAME_DESC; break; 1371 case ZmItem.F_TO: sortBy = bSortAsc ? ZmSearch.RCPT_ASC : ZmSearch.RCPT_DESC; break; 1372 case ZmItem.F_NAME: sortBy = bSortAsc ? ZmSearch.SUBJ_ASC : ZmSearch.SUBJ_DESC; break; //used for Briefcase only now. SUBJ is mappaed to the filename of the document on the server side 1373 case ZmItem.F_SUBJECT: sortBy = bSortAsc ? ZmSearch.SUBJ_ASC : ZmSearch.SUBJ_DESC; break; 1374 case ZmItem.F_DATE: sortBy = bSortAsc ? ZmSearch.DATE_ASC : ZmSearch.DATE_DESC; break; 1375 case ZmItem.F_SIZE: sortBy = bSortAsc ? ZmSearch.SIZE_ASC : ZmSearch.SIZE_DESC; break; 1376 case ZmItem.F_FLAG: sortBy = bSortAsc ? ZmSearch.FLAG_ASC : ZmSearch.FLAG_DESC; break; 1377 case ZmItem.F_ATTACHMENT: sortBy = bSortAsc ? ZmSearch.ATTACH_ASC : ZmSearch.ATTACH_DESC; break; 1378 case ZmItem.F_READ: sortBy = bSortAsc ? ZmSearch.READ_ASC : ZmSearch.READ_DESC; break; 1379 case ZmItem.F_PRIORITY: sortBy = bSortAsc ? ZmSearch.PRIORITY_ASC : ZmSearch.PRIORITY_DESC; break; 1380 case ZmItem.F_SORTED_BY: sortBy = bSortAsc ? ZmSearch.DATE_ASC : ZmSearch.DATE_DESC; break; 1381 } 1382 1383 if (sortBy) { 1384 this._currentSortColId = columnItem._sortable; 1385 //special case - switching from read/unread to another sort column - remove it from the query, so users are not confused that they still see only unread messages after clicking on another sort column. 1386 if (columnItem._sortable != ZmItem.F_READ && (this._sortByString == ZmSearch.READ_ASC || this._sortByString == ZmSearch.READ_DESC)) { 1387 var controller = this._controller; 1388 var query = controller.getSearchString(); 1389 if (query) { 1390 controller.setSearchString(AjxStringUtil.trim(query.replace("is:unread", ""))); 1391 } 1392 } 1393 this._sortByString = sortBy; 1394 var skipFirstNotify = this._folderId ? true : false; //just making it explicit boolean 1395 if (!appCtxt.isExternalAccount()) { 1396 var settings = appCtxt.getSettings(); 1397 appCtxt.set(ZmSetting.SORTING_PREF, 1398 sortBy, 1399 this.view, 1400 false, //setDefault 1401 skipFirstNotify, //skipNotify 1402 null, //account 1403 settings && !settings.persistImplicitSortPrefs(this.view)); //skipImplicit 1404 if (this._folderId) { 1405 appCtxt.set(ZmSetting.SORTING_PREF, sortBy, this._folderId); 1406 } 1407 } 1408 if (!this._isMultiColumn) { 1409 this._setSortedColStyle(columnItem._id); 1410 } 1411 } 1412 if (callback) { 1413 callback.run(); 1414 } 1415 }; 1416 1417 ZmListView.prototype._setNextSelection = 1418 function(item, forceSelection) { 1419 // set the next appropriate selected item 1420 if (this.firstSelIndex < 0) { 1421 this.firstSelIndex = 0; 1422 } 1423 if (this._list && !item) { 1424 item = this._list.get(this.firstSelIndex) || this._list.getLast(); 1425 } 1426 if (item) { 1427 this.setSelection(item, false, forceSelection); 1428 } 1429 }; 1430 1431 ZmListView.prototype._relayout = 1432 function() { 1433 DwtListView.prototype._relayout.call(this); 1434 this._checkColumns(); 1435 }; 1436 1437 ZmListView.prototype._checkColumns = 1438 function() { 1439 var numCols = this._headerList.length; 1440 var fields = []; 1441 for (var i = 0; i < numCols; i++) { 1442 var headerCol = this._headerList[i]; 1443 // bug 43540: always skip account header since its a multi-account only 1444 // column and we don't want it to sync 1445 if (headerCol && headerCol._field != ZmItem.F_ACCOUNT) { 1446 fields.push(headerCol._field + (headerCol._visible ? "" : "*")); 1447 } 1448 } 1449 var value = fields.join(ZmListView.COL_JOIN); 1450 value = (value == this._defaultCols) ? "" : value; 1451 if (!appCtxt.isExternalAccount() && !this._controller.isSearchResults) { 1452 appCtxt.set(ZmSetting.LIST_VIEW_COLUMNS, value, appCtxt.getViewTypeFromId(this.view)); 1453 } 1454 1455 this._colHeaderActionMenu = this._getActionMenuForColHeader(true); // re-create action menu so order is correct 1456 }; 1457 1458 /** 1459 * Scroll-based paging. Make sure we have at least one page of items below the visible list. 1460 * 1461 * @param ev 1462 */ 1463 ZmListView.handleScroll = 1464 function(ev) { 1465 var target = DwtUiEvent.getTarget(ev); 1466 var lv = DwtControl.findControl(target); 1467 if (lv) { 1468 lv._checkItemCount(); 1469 } 1470 }; 1471 1472 /** 1473 * Figure out if we should fetch some more items, based on where the scroll is. Our goal is to have 1474 * a certain number available below the bottom of the visible view. 1475 */ 1476 ZmListView.prototype._checkItemCount = 1477 function() { 1478 var itemsNeeded = this._getItemsNeeded(); 1479 if (itemsNeeded) { 1480 this._controller._paginate(this._view, true, null, itemsNeeded); 1481 } 1482 }; 1483 1484 /** 1485 * Figure out how many items we need to fetch to maintain a decent number 1486 * below the fold. Nonstandard list views may override. 1487 */ 1488 ZmListView.prototype._getItemsNeeded = 1489 function(skipMoreCheck) { 1490 1491 if (!skipMoreCheck) { 1492 var itemList = this.getItemList(); 1493 if (!(itemList && itemList.hasMore()) || !this._list) { return 0; } 1494 } 1495 if (!this._rendered || !this._rowHeight) { return 0; } 1496 1497 DBG.println(AjxDebug.DBG2, "List view: checking item count"); 1498 1499 var sbCallback = new AjxCallback(null, AjxTimedAction.scheduleAction, [new AjxTimedAction(this, this._resetColWidth), 100]); 1500 var params = {scrollDiv: this._getScrollDiv(), 1501 rowHeight: this._rowHeight, 1502 threshold: this.getPagelessThreshold(), 1503 limit: this.getLimit(1), 1504 listSize: this._list.size(), 1505 sbCallback: sbCallback}; 1506 return ZmListView.getRowsNeeded(params); 1507 }; 1508 1509 ZmListView.prototype._getScrollDiv = 1510 function() { 1511 return this._parentEl; 1512 }; 1513 1514 ZmListView.getRowsNeeded = 1515 function(params) { 1516 1517 var div = params.scrollDiv; 1518 var sh = div.scrollHeight, st = div.scrollTop, rh = params.rowHeight; 1519 1520 // view (porthole) height - everything measured relative to its top 1521 // prefer clientHeight since (like scrollHeight) it doesn't include borders 1522 var h = div.clientHeight || Dwt.getSize(div).y; 1523 1524 // where we'd like bottom of list view to be (with extra hidden items at bottom) 1525 var target = h + (params.threshold * rh); 1526 1527 // where bottom of list view is (including hidden items) 1528 var bottom = sh - st; 1529 1530 if (bottom == h) { 1531 // handle cases where there's no scrollbar, but we have more items (eg tall browser, or replenishment) 1532 bottom = (params.listSize * rh) - st; 1533 if (st == 0 && params.sbCallback) { 1534 // give list view a chance to fix width since it may be getting a scrollbar 1535 params.sbCallback.run(); 1536 } 1537 } 1538 1539 var rowsNeeded = 0; 1540 if (bottom < target) { 1541 // buffer below visible bottom of list view is not full 1542 rowsNeeded = Math.max(Math.floor((target - bottom) / rh), params.limit); 1543 } 1544 return rowsNeeded; 1545 }; 1546 1547 ZmListView.prototype._sizeChildren = 1548 function(height) { 1549 if (DwtListView.prototype._sizeChildren.apply(this, arguments)) { 1550 this._checkItemCount(); 1551 } 1552 }; 1553 1554 // Allow list view classes to override type used in nav text. Return null to say "items". 1555 ZmListView.prototype._getItemCountType = 1556 function() { 1557 return this.type; 1558 }; 1559 1560 /** 1561 * Checks if the given item is in this view's list. Note that the view's list may 1562 * be only part of the controller's list (the currently visible page). 1563 * 1564 * @param {String|ZmItem} item the item ID, or item to check for 1565 * @return {Boolean} <code>true</code> if the item is in the list 1566 */ 1567 ZmListView.prototype.hasItem = 1568 function(item) { 1569 1570 var id = (typeof item == "string") ? item : item && item.id; 1571 if (id && this._list) { 1572 var a = this._list.getArray(); 1573 for (var i = 0, len = a.length; i < len; i++) { 1574 var item = a[i]; 1575 if (item && item.id == id) { 1576 return true; 1577 } 1578 } 1579 } 1580 return false; 1581 }; 1582 1583 /** 1584 * The following methods allow a list view to maintain state after it has 1585 * been rerendered. State may include such elements as: which items are selected, 1586 * focus, scroll position, etc. 1587 * 1588 * @private 1589 * @param {hash} params hash of parameters: 1590 * @param {boolean} selection if true, preserve selection 1591 * @param {boolean} focus if true, preserve focus 1592 * @param {boolean} scroll if true, preserve scroll position 1593 */ 1594 ZmListView.prototype._saveState = 1595 function(params) { 1596 1597 var s = this._state = {}; 1598 params = params || {}; 1599 if (params.selection) { 1600 s.selected = this.getSelection(); 1601 if (s.selected.length == 1) { 1602 //still a special case for now till we rewrite this thing. 1603 var el = this._getElFromItem(s.selected[0]); //terribly ugly, get back to the html element so i can have access to the item data 1604 s.singleItemChecked = this._getItemData(el, ZmListView.ITEM_CHECKED_ATT_NAME); 1605 } 1606 } 1607 if (params.focus) { 1608 s.focused = this.hasFocus(); 1609 s.anchorItem = this._kbAnchor && this.getItemFromElement(this._kbAnchor); 1610 } 1611 if (params.scroll) { 1612 s.rowHeight = this._rowHeight; 1613 s.scrollTop = this._listDiv.scrollTop; 1614 } 1615 }; 1616 1617 ZmListView.prototype._restoreState = 1618 function(state) { 1619 1620 var s = state || this._state; 1621 if (s.selected && s.selected.length) { 1622 var dontCheck = s.selected.length == 1 && !s.singleItemChecked; 1623 this.setSelectedItems(s.selected, dontCheck); 1624 } 1625 if (s.anchorItem) { 1626 var el = this._getElFromItem(s.anchorItem); 1627 if (el) { 1628 this._setKbFocusElement(el); 1629 } 1630 } 1631 if (s.focused) { 1632 this.focus(); 1633 } 1634 // restore scroll position based on row height ratio 1635 if (s.rowHeight) { 1636 this._listDiv.scrollTop = s.scrollTop * (this._rowHeight / s.rowHeight); 1637 } 1638 this._state = {}; 1639 }; 1640 1641 ZmListView.prototype._renderList = 1642 function(list, noResultsOk, doAdd) { 1643 var group = this._group; 1644 if (!group) { 1645 return DwtListView.prototype._renderList.call(this, list, noResultsOk, doAdd); 1646 } 1647 if (list instanceof AjxVector && list.size()) { 1648 var now = new Date(); 1649 var size = list.size(); 1650 var htmlArr = []; 1651 var section; 1652 var headerDiv; 1653 for (var i = 0; i < size; i++) { 1654 var item = list.get(i); 1655 var div = this._createItemHtml(item, {now:now}, !doAdd, i); 1656 if (div) { 1657 if (div instanceof Array) { 1658 for (var j = 0; j < div.length; j++){ 1659 section = group.addMsgToSection(item, div[j]); 1660 if (group.getSectionSize(section) == 1){ 1661 headerDiv = this._getSectionHeaderDiv(group, section); 1662 this._addRow(headerDiv); 1663 } 1664 this._addRow(div[j]); 1665 } 1666 } else if (div.tagName || doAdd) { 1667 section = group.addMsgToSection(item, div); 1668 if (group.getSectionSize(section) == 1){ 1669 headerDiv = this._getSectionHeaderDiv(group, section); 1670 this._addRow(headerDiv); 1671 } 1672 this._addRow(div); 1673 } else { 1674 group.addMsgToSection(item, div); 1675 } 1676 } 1677 } 1678 if (group && !doAdd) { 1679 group.resetSectionHeaders(); 1680 htmlArr.push(group.getAllSections(this._bSortAsc)); 1681 } 1682 1683 if (htmlArr.length && !doAdd) { 1684 this._parentEl.innerHTML = htmlArr.join(""); 1685 } 1686 } else if (!noResultsOk) { 1687 this._setNoResultsHtml(); 1688 } 1689 1690 }; 1691 1692 ZmListView.prototype._addRow = 1693 function(row, index) { 1694 DwtListView.prototype._addRow.apply(this, arguments); 1695 1696 this._updateLabelForItem(this.getItemFromElement(row)); 1697 }; 1698 1699 ZmListView.prototype._itemAdded = function(item) { 1700 item.refCount++; 1701 }; 1702 1703 ZmListView.prototype._getSectionHeaderDiv = 1704 function(group, section) { 1705 if (group && section) { 1706 var headerDiv = document.createElement("div"); 1707 var sectionTitle = group.getSectionTitle(section); 1708 var html = group.getSectionHeader(sectionTitle); 1709 headerDiv.innerHTML = html; 1710 return headerDiv.firstChild; 1711 } 1712 }; 1713 1714 ZmListView.prototype.deactivate = 1715 function() { 1716 this._controller.inactive = true; 1717 }; 1718 1719 ZmListView.prototype._getEventTarget = 1720 function(ev) { 1721 var target = ev && ev.target; 1722 if (target && (target.nodeName === "IMG" || (target.className && target.className.match(/\bImg/)))) { 1723 return target.parentNode; 1724 } 1725 return target; 1726 }; 1727