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 defines a list controller class. 27 * 28 */ 29 30 /** 31 * This class is a base class for any controller that manages a list of items such as mail messages 32 * or contacts. It can handle alternative views of the same list. 33 * 34 * @author Conrad Damon 35 * 36 * @param {DwtControl} container the containing shell 37 * @param {ZmApp} app the containing application 38 * @param {constant} type type of controller 39 * @param {string} sessionId the session id 40 * @param {ZmSearchResultsController} searchResultsController containing controller 41 * 42 * @extends ZmBaseController 43 */ 44 ZmListController = function(container, app, type, sessionId, searchResultsController) { 45 46 if (arguments.length == 0) { return; } 47 ZmBaseController.apply(this, arguments); 48 49 // hashes keyed by view type 50 this._navToolBar = {}; // ZmNavToolBar 51 this._listView = this._view; // ZmListView (back-compatibility for bug 60073) 52 53 this._list = null; // ZmList 54 this._activeSearch = null; 55 this._newButton = null; 56 this._actionMenu = null; // ZmActionMenu 57 this._actionEv = null; 58 59 if (this.supportsDnD()) { 60 this._dropTgt = new DwtDropTarget("ZmTag"); 61 this._dropTgt.markAsMultiple(); 62 this._dropTgt.addDropListener(this._dropListener.bind(this)); 63 } 64 65 this._menuPopdownListener = this._menuPopdownActionListener.bind(this); 66 67 this._itemCountText = {}; 68 this._continuation = {count:0, totalItems:0}; 69 }; 70 71 ZmListController.prototype = new ZmBaseController; 72 ZmListController.prototype.constructor = ZmListController; 73 74 ZmListController.prototype.isZmListController = true; 75 ZmListController.prototype.toString = function() { return "ZmListController"; }; 76 77 // When performing a search action (bug 10317) on all items (including those not loaded), 78 // number of items to load on each search to work through all results. Should be a multiple 79 // of ZmList.CHUNK_SIZE. Make sure to test if you change these. 80 ZmListController.CONTINUATION_SEARCH_ITEMS = 500; 81 82 // states of the progress dialog 83 ZmListController.PROGRESS_DIALOG_INIT = "INIT"; 84 ZmListController.PROGRESS_DIALOG_UPDATE = "UPDATE"; 85 ZmListController.PROGRESS_DIALOG_CLOSE = "CLOSE"; 86 87 88 /** 89 * Performs some setup for displaying the given search results in a list view. Subclasses will need 90 * to do the actual display work, typically by calling the list view's {@link #set} method. 91 * 92 * @param {ZmSearchResult} searchResults the search results 93 */ 94 ZmListController.prototype.show = 95 function(searchResults) { 96 97 this._activeSearch = searchResults; 98 // save current search for use by replenishment 99 if (searchResults) { 100 this._currentSearch = searchResults.search; 101 this._activeSearch.viewId = this._currentSearch.viewId = this._currentViewId; 102 } 103 this.currentPage = 1; 104 this.maxPage = 1; 105 }; 106 107 /** 108 * Returns the current list view. 109 * 110 * @return {ZmListView} the list view 111 */ 112 ZmListController.prototype.getListView = 113 function() { 114 return this._view[this._currentViewId]; 115 }; 116 117 /** 118 * Gets the current search results. 119 * 120 * @return {ZmSearchResults} current search results 121 */ 122 ZmListController.prototype.getCurrentSearchResults = 123 function() { 124 return this._activeSearch; 125 }; 126 127 /** 128 * Gets the search string. 129 * 130 * @return {String} the search string 131 */ 132 ZmListController.prototype.getSearchString = 133 function() { 134 return this._currentSearch ? this._currentSearch.query : ""; 135 }; 136 137 138 ZmListController.prototype.setSearchString = 139 function(query) { 140 this._currentSearch.query = query; 141 }; 142 143 /** 144 * Gets the search string hint. 145 * 146 * @return {String} the search string hint 147 */ 148 ZmListController.prototype.getSearchStringHint = 149 function() { 150 return this._currentSearch ? this._currentSearch.queryHint : ""; 151 }; 152 153 ZmListController.prototype.getSelection = 154 function(view) { 155 view = view || this.getListView(); 156 return view ? view.getSelection() : []; 157 }; 158 159 ZmListController.prototype.getSelectionCount = 160 function(view) { 161 view = view || this.getListView(); 162 return view ? view.getSelectionCount() : 0; 163 }; 164 165 /** 166 * Gets the list. 167 * 168 * @return {ZmList} the list 169 */ 170 ZmListController.prototype.getList = 171 function() { 172 return this._list; 173 }; 174 175 /** 176 * Sets the list. 177 * 178 * @param {ZmList} newList the new list 179 */ 180 ZmListController.prototype.setList = 181 function(newList) { 182 if (newList != this._list && newList.isZmList) { 183 if (this._list) { 184 this._list.clear(); // also removes change listeners 185 } 186 this._list = newList; 187 this._list.controller = this; 188 } 189 }; 190 191 /** 192 * Sets the "has more" state. 193 * 194 * @param {Boolean} hasMore <code>true</code> if has more 195 */ 196 ZmListController.prototype.setHasMore = 197 function(hasMore) { 198 // Note: This is a bit of a HACK that is an attempt to overcome an 199 // offline issue. The problem is during initial sync when more 200 // messages come in: the forward navigation arrow doesn't get enabled. 201 202 if (hasMore && this._list) { 203 // bug: 30546 204 this._list.setHasMore(hasMore); 205 this._resetNavToolBarButtons(); 206 } 207 }; 208 209 /** 210 * Returns a list of the selected items. 211 */ 212 ZmListController.prototype.getItems = 213 function() { 214 return this.getSelection(); 215 }; 216 217 /** 218 * Returns the number of selected items. 219 */ 220 ZmListController.prototype.getItemCount = 221 function() { 222 return this.getSelectionCount(); 223 }; 224 225 /** 226 * Handles the key action. 227 * 228 * @param {constant} actionCode the action code 229 * @return {Boolean} <code>true</code> if the action is handled 230 */ 231 ZmListController.prototype.handleKeyAction = 232 function(actionCode, ev) { 233 234 DBG.println(AjxDebug.DBG3, "ZmListController.handleKeyAction"); 235 var listView = this._view[this._currentViewId]; 236 var result = false; 237 var activeEl = document.activeElement; 238 239 switch (actionCode) { 240 241 case DwtKeyMap.DBLCLICK: 242 if (activeEl && activeEl.nodeName && activeEl.nodeName.toLowerCase() === 'a') { 243 return false; 244 } 245 return listView.handleKeyAction(actionCode); 246 247 case ZmKeyMap.SHIFT_DEL: 248 case ZmKeyMap.DEL: 249 var tb = this.getCurrentToolbar(); 250 var button = tb && (tb.getButton(ZmOperation.DELETE) || tb.getButton(ZmOperation.DELETE_MENU)); 251 if (button && button.getEnabled()) { 252 this._doDelete(this.getSelection(), (actionCode == ZmKeyMap.SHIFT_DEL)); 253 result = true; 254 } 255 break; 256 257 case ZmKeyMap.NEXT_PAGE: 258 var ntb = this._navToolBar[this._currentViewId]; 259 var button = ntb ? ntb.getButton(ZmOperation.PAGE_FORWARD) : null; 260 if (button && button.getEnabled()) { 261 this._paginate(this._currentViewId, true); 262 result = true; 263 } 264 break; 265 266 case ZmKeyMap.PREV_PAGE: 267 var ntb = this._navToolBar[this._currentViewId]; 268 var button = ntb ? ntb.getButton(ZmOperation.PAGE_BACK) : null; 269 if (button && button.getEnabled()) { 270 this._paginate(this._currentViewId, false); 271 result = true; 272 } 273 break; 274 275 // Esc pops search results tab 276 case ZmKeyMap.CANCEL: 277 var ctlr = this.isSearchResults && this.searchResultsController; 278 if (ctlr) { 279 ctlr._closeListener(); 280 } 281 break; 282 283 default: 284 return ZmBaseController.prototype.handleKeyAction.apply(this, arguments); 285 } 286 return result; 287 }; 288 289 // Returns a list of desired action menu operations 290 ZmListController.prototype._getActionMenuOps = function() {}; 291 292 /** 293 * @private 294 */ 295 ZmListController.prototype._standardActionMenuOps = 296 function() { 297 return [ZmOperation.TAG_MENU, ZmOperation.MOVE, ZmOperation.PRINT]; 298 }; 299 300 /** 301 * @private 302 */ 303 ZmListController.prototype._participantOps = 304 function() { 305 var ops = [ZmOperation.SEARCH_MENU]; 306 307 if (appCtxt.get(ZmSetting.MAIL_ENABLED)) { 308 ops.push(ZmOperation.NEW_MESSAGE); 309 } 310 311 if (appCtxt.get(ZmSetting.CONTACTS_ENABLED)) { 312 ops.push(ZmOperation.CONTACT); 313 } 314 315 return ops; 316 }; 317 318 /** 319 * Initializes action menu: menu items and listeners 320 * 321 * @private 322 */ 323 ZmListController.prototype._initializeActionMenu = 324 function() { 325 326 if (this._actionMenu) { return; } 327 328 var menuItems = this._getActionMenuOps(); 329 if (!menuItems) { return; } 330 331 var menuParams = {parent:this._shell, 332 menuItems: menuItems, 333 context: this._getMenuContext(), 334 controller: this 335 }; 336 this._actionMenu = new ZmActionMenu(menuParams); 337 this._addMenuListeners(this._actionMenu); 338 if (appCtxt.get(ZmSetting.TAGGING_ENABLED)) { 339 this._setupTagMenu(this._actionMenu); 340 } 341 }; 342 343 /** 344 * Sets up tab groups (focus ring). 345 * 346 * @private 347 */ 348 ZmListController.prototype._initializeTabGroup = 349 function(view) { 350 if (this._tabGroups[view]) { return; } 351 352 ZmBaseController.prototype._initializeTabGroup.apply(this, arguments); 353 354 var navToolBar = this._navToolBar[view]; 355 if (navToolBar) { 356 this._tabGroups[view].addMember(navToolBar.getTabGroupMember()); 357 } 358 }; 359 360 /** 361 * Gets the tab group. 362 * 363 * @return {Object} the tab group 364 */ 365 ZmListController.prototype.getTabGroup = 366 function() { 367 return this._tabGroups[this._currentViewId]; 368 }; 369 370 /** 371 * @private 372 */ 373 ZmListController.prototype._addMenuListeners = 374 function(menu) { 375 376 var menuItems = menu.opList; 377 for (var i = 0; i < menuItems.length; i++) { 378 var menuItem = menuItems[i]; 379 if (this._listeners[menuItem]) { 380 menu.addSelectionListener(menuItem, this._listeners[menuItem], 0); 381 } 382 } 383 menu.addPopdownListener(this._menuPopdownListener); 384 }; 385 386 ZmListController.prototype._menuPopdownActionListener = 387 function(ev) { 388 389 var view = this.getListView(); 390 if (!this._pendingActionData) { 391 if (view && view.handleActionPopdown) { 392 view.handleActionPopdown(ev); 393 } 394 } 395 // Reset back to item count unless there is multiple selection 396 var selCount = view ? view.getSelectionCount() : -1; 397 if (selCount <= 1) { 398 this._setItemCountText(); 399 } 400 }; 401 402 403 404 // List listeners 405 406 /** 407 * List selection event - handle flagging if a flag icon was clicked, otherwise 408 * reset the toolbar based on how many items are selected. 409 * 410 * @private 411 */ 412 ZmListController.prototype._listSelectionListener = 413 function(ev) { 414 415 if (ev.field == ZmItem.F_FLAG) { 416 this._doFlag([ev.item]); 417 return true; 418 } 419 else { 420 var lv = this._listView[this._currentViewId]; 421 if (lv) { 422 if (appCtxt.get(ZmSetting.SHOW_SELECTION_CHECKBOX) && !ev.ctrlKey) { 423 if (lv.setSelectionHdrCbox) { 424 lv.setSelectionHdrCbox(false); 425 } 426 } 427 this._resetOperations(this.getCurrentToolbar(), lv.getSelectionCount()); 428 if (ev.shiftKey) { 429 this._setItemSelectionCountText(); 430 } 431 else { 432 this._setItemCountText(); 433 } 434 } 435 } 436 return false; 437 }; 438 439 /** 440 * List action event - set the dynamic tag menu, and enable operations in the 441 * action menu based on the number of selected items. Note that the menu is not 442 * actually popped up here; that's left up to the subclass, which should 443 * override this function. 444 * 445 * @private 446 */ 447 ZmListController.prototype._listActionListener = 448 function(ev) { 449 450 this._actionEv = ev; 451 var actionMenu = this.getActionMenu(); 452 if (appCtxt.get(ZmSetting.TAGGING_ENABLED)) { 453 this._setTagMenu(actionMenu); 454 } 455 456 if (appCtxt.get(ZmSetting.SEARCH_ENABLED)) { 457 this._setSearchMenu(actionMenu); 458 } 459 this._resetOperations(actionMenu, this.getSelectionCount()); 460 this._setItemSelectionCountText(); 461 }; 462 463 464 // Navbar listeners 465 466 /** 467 * @private 468 */ 469 ZmListController.prototype._navBarListener = 470 function(ev) { 471 472 // skip listener for non-current views 473 if (!this.isCurrent()) { return; } 474 475 var op = ev.item.getData(ZmOperation.KEY_ID); 476 477 if (op == ZmOperation.PAGE_BACK || op == ZmOperation.PAGE_FORWARD) { 478 this._paginate(this._currentViewId, (op == ZmOperation.PAGE_FORWARD)); 479 } 480 }; 481 482 // Drag and drop listeners 483 484 /** 485 * @private 486 */ 487 ZmListController.prototype._dragListener = 488 function(ev) { 489 490 if (this.isSearchResults && ev.action == DwtDragEvent.DRAG_START) { 491 this.searchResultsController.showOverview(true); 492 } 493 else if (ev.action == DwtDragEvent.SET_DATA) { 494 ev.srcData = {data: ev.srcControl.getDnDSelection(), controller: this}; 495 } 496 else if (this.isSearchResults && (ev.action == DwtDragEvent.DRAG_END || ev.action == DwtDragEvent.DRAG_CANCEL)) { 497 this.searchResultsController.showOverview(false); 498 } 499 }; 500 501 /** 502 * The list view as a whole is the drop target, since it's the lowest-level widget. Still, we 503 * need to find out which item got dropped onto, so we get that from the original UI event 504 * (a mouseup). The header is within the list view, but not an item, so it's not a valid drop 505 * target. One drawback of having the list view be the drop target is that we can't exercise 506 * fine-grained control on what's a valid drop target. If you enter via an item and then drag to 507 * the header, it will appear to be valid. 508 * 509 * @protected 510 */ 511 ZmListController.prototype._dropListener = 512 function(ev) { 513 514 var view = this._view[this._currentViewId]; 515 var div = view.getTargetItemDiv(ev.uiEvent); 516 var item = view.getItemFromElement(div); 517 518 // only tags can be dropped on us 519 var data = ev.srcData.data; 520 if (ev.action == DwtDropEvent.DRAG_ENTER) { 521 ev.doIt = (item && (item instanceof ZmItem) && !item.isReadOnly() && this._dropTgt.isValidTarget(data)); 522 // Bug: 44488 - Don't allow dropping tag of one account to other account's item 523 if (appCtxt.multiAccounts) { 524 var listAcctId = item ? item.getAccount().id : null; 525 var tagAcctId = (data.account && data.account.id) || data[0].account.id; 526 if (listAcctId != tagAcctId) { 527 ev.doIt = false; 528 } 529 } 530 DBG.println(AjxDebug.DBG3, "DRAG_ENTER: doIt = " + ev.doIt); 531 if (ev.doIt) { 532 view.dragSelect(div); 533 } 534 } else if (ev.action == DwtDropEvent.DRAG_DROP) { 535 view.dragDeselect(div); 536 var items = [item]; 537 var sel = this.getSelection(); 538 if (sel.length) { 539 var vec = AjxVector.fromArray(sel); 540 if (vec.contains(item)) { 541 items = sel; 542 } 543 } 544 this._doTag(items, data, true); 545 } else if (ev.action == DwtDropEvent.DRAG_LEAVE) { 546 view.dragDeselect(div); 547 } else if (ev.action == DwtDropEvent.DRAG_OP_CHANGED) { 548 // nothing 549 } 550 }; 551 552 /** 553 * @private 554 */ 555 556 /** 557 * returns true if the search folder is drafts 558 */ 559 ZmListController.prototype.isDraftsFolder = 560 function() { 561 var folder = this._getSearchFolder(); 562 if (!folder) { 563 return false; 564 } 565 return folder.nId == ZmFolder.ID_DRAFTS; 566 }; 567 568 /** 569 * returns true if the search folder is drafts 570 */ 571 ZmListController.prototype.isOutboxFolder = 572 function() { 573 var folder = this._getSearchFolder(); 574 if (!folder) { 575 return false; 576 } 577 return folder.nId == ZmFolder.ID_OUTBOX; 578 }; 579 580 /** 581 * returns true if the search folder is sync failures 582 */ 583 ZmListController.prototype.isSyncFailuresFolder = 584 function() { 585 var folder = this._getSearchFolder(); 586 if (!folder) { 587 return false; 588 } 589 return folder.nId == ZmFolder.ID_SYNC_FAILURES; 590 }; 591 592 593 // Actions on items are performed through their containing list 594 ZmListController.prototype._getList = 595 function(items) { 596 597 var list = ZmBaseController.prototype._getList.apply(this, arguments); 598 if (!list) { 599 list = this._list; 600 } 601 602 return list; 603 }; 604 605 // if items were removed, see if we need to fetch more 606 ZmListController.prototype._getAllDoneCallback = 607 function() { 608 return this._checkItemCount.bind(this); 609 }; 610 611 /** 612 * Manages the progress dialog that appears when an action is performed on a large number of items. 613 * The arguments include a state and any arguments relative to that state. The state is one of: 614 * 615 * ZmListController.PROGRESS_DIALOG_INIT 616 * ZmListController.PROGRESS_DIALOG_UPDATE 617 * ZmListController.PROGRESS_DIALOG_CLOSE 618 * 619 * @param {hash} params a hash of params: 620 * @param {constant} state state of the dialog 621 * @param {AjxCallback} callback cancel callback (INIT) 622 * @param {string} summary summary text (UPDATE) 623 */ 624 ZmListController.handleProgress = 625 function(params) { 626 627 var dialog = appCtxt.getCancelMsgDialog(); 628 if (params.state == ZmListController.PROGRESS_DIALOG_INIT) { 629 dialog.reset(); 630 dialog.registerCallback(DwtDialog.CANCEL_BUTTON, params.callback); 631 ZmListController.progressDialogReady = true; 632 } 633 else if (params.state == ZmListController.PROGRESS_DIALOG_UPDATE && ZmListController.progressDialogReady) { 634 dialog.setMessage(params.summary, DwtMessageDialog.INFO_STYLE, AjxMessageFormat.format(ZmMsg.inProgress)); 635 if (!dialog.isPoppedUp()) { 636 dialog.popup(); 637 } 638 } 639 else if (params.state == ZmListController.PROGRESS_DIALOG_CLOSE) { 640 dialog.unregisterCallback(DwtDialog.CANCEL_BUTTON); 641 dialog.popdown(); 642 ZmListController.progressDialogReady = false; 643 } 644 }; 645 646 647 // Pagination 648 649 /** 650 * @private 651 */ 652 ZmListController.prototype._cacheList = 653 function(search, offset) { 654 655 if (this._list) { 656 var newList = search.getResults().getVector(); 657 offset = offset ? offset : parseInt(search.getAttribute("offset")); 658 this._list.cache(offset, newList); 659 } else { 660 this._list = search.getResults(type); 661 } 662 }; 663 664 /** 665 * @private 666 */ 667 ZmListController.prototype._search = 668 function(view, offset, limit, callback, isCurrent, lastId, lastSortVal) { 669 var originalSearch = this._activeSearch && this._activeSearch.search; 670 var params = { 671 query: this.getSearchString(), 672 queryHint: this.getSearchStringHint(), 673 types: originalSearch && originalSearch.types || [], // use types from original search 674 userInitiated: originalSearch && originalSearch.userInitiated, 675 sortBy: appCtxt.get(ZmSetting.SORTING_PREF, view), 676 offset: offset, 677 limit: limit, 678 lastId: lastId, 679 lastSortVal: lastSortVal 680 }; 681 // add any additional params... 682 this._getMoreSearchParams(params); 683 684 var search = new ZmSearch(params); 685 if (isCurrent) { 686 this._currentSearch = search; 687 } 688 689 appCtxt.getSearchController().redoSearch(search, true, null, callback); 690 }; 691 692 /** 693 * Gets next or previous page of items. The set of items may come from the 694 * cached list, or from the server (using the current search as a base). 695 * <p> 696 * The loadIndex is the index'd item w/in the list that needs to be loaded - 697 * initiated only when user is in CV and pages a conversation that has not 698 * been loaded yet.</p> 699 * <p> 700 * Note that this method returns a value even though it may make an 701 * asynchronous SOAP request. That's possible as long as no caller 702 * depends on the results of that request. Currently, the only caller that 703 * looks at the return value acts on it only if no request was made.</p> 704 * 705 * @param {constant} view the current view 706 * @param {Boolean} forward if <code>true</code>, get next page rather than previous 707 * @param {int} loadIndex the index of item to show 708 * @param {int} limit the number of items to fetch 709 * 710 * @private 711 */ 712 ZmListController.prototype._paginate = 713 function(view, forward, loadIndex, limit) { 714 715 var needMore = false; 716 var lv = this._view[view]; 717 if (!lv) { return; } 718 var offset, max; 719 720 limit = limit || lv.getLimit(offset); 721 722 if (lv._isPageless) { 723 offset = this._list.size(); 724 needMore = true; 725 } else { 726 offset = lv.getNewOffset(forward); 727 needMore = (offset + limit > this._list.size()); 728 this.currentPage = this.currentPage + (forward ? 1 : -1); 729 this.maxPage = Math.max(this.maxPage, this.currentPage); 730 } 731 732 // see if we're out of items and the server has more 733 if (needMore && this._list.hasMore()) { 734 lv.offset = offset; // cache new offset 735 if (lv._isPageless) { 736 max = limit; 737 } else { 738 // figure out how many items we need to fetch 739 var delta = (offset + limit) - this._list.size(); 740 max = delta < limit && delta > 0 ? delta : limit; 741 if (max < limit) { 742 offset = ((offset + limit) - max) + 1; 743 } 744 } 745 746 // handle race condition - user has paged quickly and we don't want 747 // to do second fetch while one is pending 748 if (this._searchPending) { return false; } 749 750 // figure out if this requires cursor-based paging 751 var list = lv.getList(); 752 var lastItem = list && list.getLast(); 753 var lastSortVal = (lastItem && lastItem.id) ? lastItem.sf : null; 754 var lastId = lastSortVal ? lastItem.id : null; 755 756 this._setItemCountText(ZmMsg.loading); 757 758 // get next page of items from server; note that callback may be overridden 759 this._searchPending = true; 760 var respCallback = this._handleResponsePaginate.bind(this, view, false, loadIndex, offset); 761 this._search(view, offset, max, respCallback, true, lastId, lastSortVal); 762 return false; 763 } else if (!lv._isPageless) { 764 lv.offset = offset; // cache new offset 765 this._resetOperations(this._toolbar[view], 0); 766 this._resetNavToolBarButtons(view); 767 this._setViewContents(view); 768 this._resetSelection(); 769 return true; 770 } 771 }; 772 773 /** 774 * Updates the list and the view after a new page of items has been retrieved. 775 * 776 * @param {constant} view the current view 777 * @param {Boolean} saveSelection if <code>true</code>, maintain current selection 778 * @param {int} loadIndex the index of item to show 779 * @param {ZmCsfeResult} result the result of SOAP request 780 * @param {Boolean} ignoreResetSelection if <code>true</code>, do not reset selection 781 * 782 * @private 783 */ 784 ZmListController.prototype._handleResponsePaginate = 785 function(view, saveSelection, loadIndex, offset, result, ignoreResetSelection) { 786 787 var searchResult = result.getResponse(); 788 789 // update "more" flag 790 this._list.setHasMore(searchResult.getAttribute("more")); 791 792 this._cacheList(searchResult, offset); 793 794 var lv = this._view[this._currentViewId]; 795 var num = lv._isPageless ? this.getSelectionCount() : 0; 796 this._resetOperations(this._toolbar[view], num); 797 798 // remember selected index if told to 799 var selItem = saveSelection ? this.getSelection()[0] : null; 800 var selectedIdx = selItem ? lv.getItemIndex(selItem) : -1; 801 802 var items = searchResult && searchResult.getResults().getArray(); 803 if (lv._isPageless && items && items.length) { 804 lv._itemsToAdd = items; 805 } else { 806 lv._itemsToAdd = null; 807 } 808 var wasEmpty = (lv._isPageless && (lv.size() == 0)); 809 810 this._setViewContents(view); 811 812 // add new items to selection if all results selected, in a way that doesn't call deselectAll() 813 if (lv.allSelected) { 814 for (var i = 0, len = items.length; i < len; i++) { 815 lv.selectItem(items[i], true); 816 lv.setSelectionCbox(items[i], false); 817 } 818 lv.setSelectionHdrCbox(true); 819 DBG.println("scr", "pagination - selected more items: " + items.length); 820 DBG.println("scr", "items selected: " + this.getSelectionCount()); 821 } 822 this._resetNavToolBarButtons(view); 823 824 // bug fix #5134 - some views may not want to reset the current selection 825 if (!ignoreResetSelection && !lv._isPageless) { 826 this._resetSelection(selectedIdx); 827 } else if (wasEmpty) { 828 lv._setNextSelection(); 829 } 830 831 this._searchPending = false; 832 }; 833 834 /** 835 * @private 836 */ 837 ZmListController.prototype._getMoreSearchParams = 838 function(params) { 839 // overload me if more params are needed for SearchRequest 840 }; 841 842 /** 843 * @private 844 */ 845 ZmListController.prototype._checkReplenish = 846 function(callback) { 847 848 var view = this.getListView(); 849 var list = view.getList(); 850 // don't bother if the view doesn't really have a list 851 var replenishmentDone = false; 852 if (list) { 853 var replCount = view.getLimit() - view.size(); 854 if (replCount > view.getReplenishThreshold()) { 855 this._replenishList(this._currentViewId, replCount, callback); 856 replenishmentDone = true; 857 } 858 } 859 if (callback && !replenishmentDone) { 860 callback.run(); 861 } 862 }; 863 864 /** 865 * All items in the list view are gone - show "No Results". 866 * 867 * @private 868 */ 869 ZmListController.prototype._handleEmptyList = 870 function(listView) { 871 if (this.currentPage > 1) { 872 this._paginate(this._currentViewId, false, 0); 873 } else { 874 listView.removeAll(true); 875 listView._setNoResultsHtml(); 876 this._resetNavToolBarButtons(); 877 listView._checkItemCount(); 878 } 879 }; 880 881 /** 882 * @private 883 */ 884 ZmListController.prototype._replenishList = 885 function(view, replCount, callback) { 886 887 // determine if there are any more items to replenish with 888 var idxStart = this._view[view].offset + this._view[view].size(); 889 var totalCount = this._list.size(); 890 891 if (idxStart < totalCount) { 892 // replenish from cache 893 var idxEnd = (idxEnd > totalCount) ? totalCount : (idxStart + replCount); 894 var list = this._list.getVector().getArray(); 895 var sublist = list.slice(idxStart, idxEnd); 896 var subVector = AjxVector.fromArray(sublist); 897 this._view[view].replenish(subVector); 898 if (callback) { 899 callback.run(); 900 } 901 } else { 902 // replenish from server request 903 this._getMoreToReplenish(view, replCount, callback); 904 } 905 }; 906 907 /** 908 * @private 909 */ 910 ZmListController.prototype._resetSelection = 911 function(idx) { 912 var list = this.getListView().getList(); 913 if (list) { 914 var selIdx = idx >= 0 ? idx : 0; 915 var first = list.get(selIdx); 916 this._view[this._currentViewId].setSelection(first, false); 917 } 918 }; 919 920 /** 921 * Requests replCount items from the server to replenish current listview. 922 * 923 * @param {constant} view the current view to replenish 924 * @param {int} replCount the number of items to replenish 925 * @param {AjxCallback} callback the async callback 926 * 927 * @private 928 */ 929 ZmListController.prototype._getMoreToReplenish = 930 function(view, replCount, callback) { 931 932 if (this._list.hasMore()) { 933 // use a cursor if we can 934 var list = this._view[view].getList(); 935 var lastItem = list.getLast(); 936 var lastSortVal = (lastItem && lastItem.id) ? lastItem.sf : null; 937 var lastId = lastSortVal ? lastItem.id : null; 938 var respCallback = this._handleResponseGetMoreToReplenish.bind(this, view, callback); 939 this._search(view, this._list.size(), replCount, respCallback, false, lastId, lastSortVal); 940 } else { 941 if (callback) { 942 callback.run(); 943 } 944 } 945 }; 946 947 /** 948 * @private 949 */ 950 ZmListController.prototype._handleResponseGetMoreToReplenish = 951 function(view, callback, result) { 952 953 var searchResult = result.getResponse(); 954 955 // set updated has more flag 956 var more = searchResult.getAttribute("more"); 957 this._list.setHasMore(more); 958 959 // cache search results into internal list 960 this._cacheList(searchResult); 961 962 // update view w/ replenished items 963 var list = searchResult.getResults().getVector(); 964 this._view[view].replenish(list); 965 966 // reset forward pagination button only 967 this._toolbar[view].enable(ZmOperation.PAGE_FORWARD, more); 968 969 if (callback) { 970 callback.run(result); 971 } 972 }; 973 974 ZmListController.prototype._initializeNavToolBar = 975 function(view) { 976 var tb = new ZmNavToolBar({parent:this._toolbar[view], context:view}); 977 this._setNavToolBar(tb, view); 978 }; 979 980 ZmListController.prototype._setNavToolBar = 981 function(toolbar, view) { 982 this._navToolBar[view] = toolbar; 983 if (this._navToolBar[view]) { 984 var navBarListener = this._navBarListener.bind(this); 985 this._navToolBar[view].addSelectionListener(ZmOperation.PAGE_BACK, navBarListener); 986 this._navToolBar[view].addSelectionListener(ZmOperation.PAGE_FORWARD, navBarListener); 987 } 988 }; 989 990 /** 991 * @private 992 */ 993 ZmListController.prototype._resetNavToolBarButtons = 994 function(view) { 995 996 var lv; 997 if (view) { 998 lv = this._view[view]; 999 } else { 1000 lv = this.getListView(); 1001 view = this._currentViewId; 1002 } 1003 if (!lv) { return; } 1004 1005 if (lv._isPageless) { 1006 this._setItemCountText(); 1007 } 1008 1009 if (!this._navToolBar[view]) { return; } 1010 1011 this._navToolBar[view].enable(ZmOperation.PAGE_BACK, lv.offset > 0); 1012 1013 // determine if we have more cached items to show (in case hasMore is wrong) 1014 var hasMore = false; 1015 if (this._list) { 1016 hasMore = this._list.hasMore(); 1017 if (!hasMore && ((lv.offset + lv.getLimit()) < this._list.size())) { 1018 hasMore = true; 1019 } 1020 } 1021 1022 this._navToolBar[view].enable(ZmOperation.PAGE_FORWARD, hasMore); 1023 1024 this._navToolBar[view].setText(this._getNavText(view)); 1025 }; 1026 1027 /** 1028 * @private 1029 */ 1030 ZmListController.prototype.enablePagination = 1031 function(enabled, view) { 1032 1033 if (!this._navToolBar[view]) { return; } 1034 1035 if (enabled) { 1036 this._resetNavToolBarButtons(view); 1037 } else { 1038 this._navToolBar[view].enable([ZmOperation.PAGE_BACK, ZmOperation.PAGE_FORWARD], false); 1039 } 1040 }; 1041 1042 /** 1043 * @private 1044 */ 1045 ZmListController.prototype._getNavText = 1046 function(view) { 1047 1048 var se = this._getNavStartEnd(view); 1049 if (!se) { return ""; } 1050 1051 var size = se.size; 1052 var msg = ""; 1053 if (size === 0) { 1054 msg = AjxMessageFormat.format(ZmMsg.navTextNoItems, ZmMsg[ZmApp.NAME[ZmApp.TASKS]]); 1055 } else if (size === 1) { 1056 msg = AjxMessageFormat.format(ZmMsg.navTextOneItem, ZmMsg[ZmItem.MSG_KEY[ZmItem.TASK]]); 1057 } else { 1058 // Multiple items 1059 var lv = this._view[view]; 1060 var limit = se.limit; 1061 if (size < limit) { 1062 // We have the exact size of the filtered items 1063 msg = AjxMessageFormat.format(ZmMsg.navTextWithTotal, [se.start, se.end, size]); 1064 } else { 1065 // If it's more than the limit, we don't have an exact count 1066 // available from the server 1067 var sizeText = this._getUpperLimitSizeText(size); 1068 var msgText = sizeText ? ZmMsg.navTextWithTotal : ZmMsg.navTextRange; 1069 msg = AjxMessageFormat.format(msgText, [se.start, se.end, sizeText]); 1070 } 1071 } 1072 return msg; 1073 }; 1074 1075 /** 1076 * @private 1077 */ 1078 ZmListController.prototype._getNavStartEnd = 1079 function(view) { 1080 1081 var lv = this._view[view]; 1082 var limit = lv.getLimit(); 1083 var size = this._list ? this._list.size() : 0; 1084 1085 var start, end; 1086 if (size > 0) { 1087 start = lv.offset + 1; 1088 end = Math.min(lv.offset + limit, size); 1089 } 1090 1091 return (start && end) ? {start:start, end:end, size:size, limit:limit} : null; 1092 }; 1093 1094 /** 1095 * @private 1096 */ 1097 ZmListController.prototype._getNumTotal = 1098 function() { 1099 1100 var folderId = this._getSearchFolderId(); 1101 if (folderId && (folderId != ZmFolder.ID_TRASH)) { 1102 var folder = appCtxt.getById(folderId); 1103 if (folder) { 1104 return folder.numTotal; 1105 } 1106 } 1107 return null; 1108 }; 1109 1110 /** 1111 * @private 1112 */ 1113 ZmListController.prototype.getActionMenu = 1114 function() { 1115 if (!this._actionMenu) { 1116 this._initializeActionMenu(); 1117 //DBG.timePt("_initializeActionMenu"); 1118 this._resetOperations(this._actionMenu, 0); 1119 //DBG.timePt("this._resetOperation(actionMenu)"); 1120 } 1121 return this._actionMenu; 1122 }; 1123 1124 /** 1125 * Returns the context for the action menu created by this controller (used to create 1126 * an ID for the menu). 1127 * 1128 * @private 1129 */ 1130 ZmListController.prototype._getMenuContext = 1131 function() { 1132 return this._app && this._app._name; 1133 }; 1134 1135 /** 1136 * @private 1137 */ 1138 ZmListController.prototype._getItemCountText = 1139 function() { 1140 1141 var size = this._getItemCount(); 1142 // Size can be null or a number 1143 if (!size) { return ""; } 1144 1145 var lv = this._view[this._currentViewId], 1146 list = lv && lv._list, 1147 type = lv._getItemCountType(), 1148 total = this._getNumTotal(), 1149 num = total || size, 1150 countKey = 'type' + AjxStringUtil.capitalizeFirstLetter(ZmItem.MSG_KEY[type]), 1151 typeText = type ? AjxMessageFormat.format(ZmMsg[countKey], num) : ""; 1152 1153 if (total && (size != total)) { 1154 return AjxMessageFormat.format(ZmMsg.itemCount1, [size, total, typeText]); 1155 } else { 1156 var sizeText = this._getUpperLimitSizeText(size); 1157 return AjxMessageFormat.format(ZmMsg.itemCount, [sizeText, typeText]); 1158 } 1159 }; 1160 1161 ZmListController.prototype._getUpperLimitSizeText = 1162 function(size) { 1163 var sizeText = size; 1164 if (this._list.hasMore()) { 1165 //show 4+, 5+, 10+, 20+, 100+, 200+ 1166 var granularity = size < 10 ? 1 : size < 100 ? 10 : 100; 1167 sizeText = (Math.floor(size / granularity)) * granularity + "+"; //round down to the chosen granularity 1168 } 1169 return sizeText; 1170 1171 } 1172 1173 1174 1175 ZmListController.prototype._getItemCount = 1176 function() { 1177 var lv = this.getListView(); 1178 var list = lv && lv._list; 1179 if (!list) { 1180 return 0; 1181 } 1182 return list.size(); 1183 }; 1184 1185 /** 1186 * Sets the text that shows the number of items, if we are pageless. 1187 * 1188 * @private 1189 */ 1190 ZmListController.prototype._setItemCountText = 1191 function(text) { 1192 1193 text = text || this._getItemCountText(); 1194 var field = this._itemCountText[this._currentViewId]; 1195 if (field) { 1196 field.setText(text); 1197 } 1198 }; 1199 1200 // Returns text that describes how many items are selected for action 1201 ZmListController.prototype._getItemSelectionCountText = function() { 1202 1203 var lv = this._view[this._currentViewId], 1204 list = lv && lv._list, 1205 type = lv._getItemCountType(), 1206 num = lv.getSelectionCount(), 1207 countKey = 'type' + AjxStringUtil.capitalizeFirstLetter(ZmItem.MSG_KEY[type]), 1208 typeText = type ? AjxMessageFormat.format(ZmMsg[countKey], num) : ""; 1209 1210 return num > 0 ? AjxMessageFormat.format(ZmMsg.itemSelectionCount, [num, typeText]) : ''; 1211 }; 1212 1213 ZmListController.prototype._setItemSelectionCountText = function() { 1214 this._setItemCountText(this._getItemSelectionCountText()); 1215 }; 1216 1217 /** 1218 * Records total items and last item before we do any more searches. Adds a couple 1219 * params to the args for the list action method. 1220 * 1221 * @param {function} actionMethod the controller action method 1222 * @param {Array} args an arg list for above (except for items arg) 1223 * @param {Hash} params the params that will be passed to list action method 1224 * @param {closure} allDoneCallback the callback to run after all items processed 1225 * 1226 * @private 1227 */ 1228 ZmListController.prototype._setupContinuation = 1229 function(actionMethod, args, params, allDoneCallback, notIdsOnly) { 1230 1231 // need to use AjxCallback here so we can prepend items arg when calling it 1232 var actionCallback = new AjxCallback(this, actionMethod, args); 1233 params.finalCallback = this._continueAction.bind(this, {actionCallback:actionCallback, allDoneCallback:allDoneCallback, notIdsOnly: notIdsOnly}); 1234 1235 params.count = this._continuation.count; 1236 params.idsOnly = !notIdsOnly; 1237 1238 if (!this._continuation.lastItem) { 1239 this._continuation.lastItem = params.list.getVector().getLast(); 1240 this._continuation.totalItems = params.list.size(); 1241 } 1242 }; 1243 1244 /** 1245 * See if we are performing an action on all items, including ones that match the current search 1246 * but have not yet been retrieved. If so, keep doing searches and performing the action on the 1247 * results, until there are no more results. 1248 * 1249 * The arguments in the action callback should be those after the initial 'items' argument. The 1250 * array of items retrieved by the search is prepended to the callback's argument list before it 1251 * is run. 1252 * 1253 * @param {Hash} params a hash of parameters 1254 * @param {AjxCallback} actionCallback the callback with action to be performed on search results 1255 * @param {closure} allDoneCallback the callback to run when we're all done 1256 * @param {Hash} actionParams the params from <code>ZmList._itemAction</code>, added when this is called 1257 * 1258 * @private 1259 */ 1260 ZmListController.prototype._continueAction = 1261 function(params, actionParams) { 1262 1263 var lv = this._view[this._currentViewId]; 1264 var cancelled = actionParams && actionParams.cancelled; 1265 var contResult = this._continuation.result; 1266 var hasMore = contResult ? contResult.getAttribute("more") : (this._list ? this._list.hasMore() : false); 1267 DBG.println("sa", "lv.allSelected: " + lv.allSelected + ", hasMore: " + hasMore); 1268 if (lv.allSelected && hasMore && !cancelled) { 1269 var cs = this._currentSearch; 1270 var limit = ZmListController.CONTINUATION_SEARCH_ITEMS; 1271 var searchParams = { 1272 query: this.getSearchString(), 1273 queryHint: this.getSearchStringHint(), 1274 types: cs.types, 1275 sortBy: cs.sortBy, 1276 limit: limit, 1277 idsOnly: !params.notIdsOnly 1278 }; 1279 1280 var list = contResult ? contResult.getResults() : this._list.getArray(); 1281 var lastItem = this._continuation.lastItem; 1282 if (!lastItem) { 1283 lastItem = list && list[list.length - 1]; 1284 } 1285 if (lastItem) { 1286 searchParams.lastId = lastItem.id; 1287 searchParams.lastSortVal = lastItem.sf; 1288 DBG.println("sa", "***** continuation search: " + searchParams.query + " --- " + [lastItem.id, lastItem.sf].join("/")); 1289 } else { 1290 searchParams.offset = limit + (this._continuation.search ? this._continuation.search.offset : 0); 1291 } 1292 1293 this._continuation.count = actionParams.numItems; 1294 if (!this._continuation.totalItems) { 1295 this._continuation.totalItems = list.length; 1296 } 1297 1298 this._continuation.search = new ZmSearch(searchParams); 1299 var respCallback = this._handleResponseContinueAction.bind(this, params.actionCallback); 1300 appCtxt.getSearchController().redoSearch(this._continuation.search, true, null, respCallback); 1301 } else { 1302 DBG.println("sa", "end of continuation"); 1303 if (contResult) { 1304 if (lv.allSelected) { 1305 // items beyond page were acted on, give user a total count 1306 if (actionParams.actionTextKey) { 1307 var type = contResult.type; 1308 if (type === ZmId.SEARCH_MAIL) { 1309 type = this._list.type; //get the specific CONV/MSG type instead of the "searchFor" "MAIL". 1310 } 1311 actionParams.actionSummary = ZmList.getActionSummary({ 1312 actionTextKey: actionParams.actionTextKey, 1313 numItems: this._continuation.totalItems, 1314 type: type, 1315 actionArg: actionParams.actionArg 1316 }); 1317 } 1318 lv.deselectAll(); 1319 } 1320 this._continuation = {count:0, totalItems:0}; 1321 } 1322 if (params.allDoneCallback) { 1323 params.allDoneCallback(); 1324 } 1325 1326 ZmListController.handleProgress({state:ZmListController.PROGRESS_DIALOG_CLOSE}); 1327 ZmBaseController.showSummary(actionParams.actionSummary, actionParams.actionLogItem, actionParams.closeChildWin); 1328 } 1329 }; 1330 1331 /** 1332 * @private 1333 */ 1334 ZmListController.prototype._handleResponseContinueAction = 1335 function(actionCallback, result) { 1336 1337 this._continuation.result = result.getResponse(); 1338 var items = this._continuation.result.getResults(); 1339 DBG.println("sa", "continuation search results: " + items.length); 1340 if (items.isZmMailList) { //no idsOnly case 1341 items = items.getArray(); 1342 } 1343 if (items.length) { 1344 this._continuation.lastItem = items[items.length - 1]; 1345 this._continuation.totalItems += items.length; 1346 DBG.println("sa", "continuation last item: " + this._continuation.lastItem.id); 1347 actionCallback.args = actionCallback.args || []; 1348 actionCallback.args.unshift(items); 1349 DBG.println("sa", "calling continuation action on search results"); 1350 actionCallback.run(); 1351 } else { 1352 DBG.println(AjxDebug.DBG1, "Continuation with empty search results!"); 1353 } 1354 }; 1355 1356 /** 1357 * @private 1358 */ 1359 ZmListController.prototype._checkItemCount = 1360 function() { 1361 var lv = this._view[this._currentViewId]; 1362 lv._checkItemCount(); 1363 lv._handleResponseCheckReplenish(true); 1364 }; 1365 1366 // Returns true if this controller supports sorting its items 1367 ZmListController.prototype.supportsSorting = function() { 1368 return true; 1369 }; 1370 1371 // Returns true if this controller supports alternatively grouped list views 1372 ZmListController.prototype.supportsGrouping = function() { 1373 return false; 1374 }; 1375