1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2011, 2012, 2013, 2014, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * @overview 26 * This file defines the search results controller. 27 * 28 * @author Conrad Damon 29 */ 30 31 /** 32 * @class 33 * This controller is used to display the results of a user-initiated search. The results are 34 * displayed in a tab which has three parts: a search bar at the top that can be used to modify 35 * the search, a filtering mechanism on the left that can be used to refine the search, and the 36 * results themselves. The results may be of any type: messages, contacts, etc. 37 * 38 * @param {DwtShell} container the application container 39 * @param {ZmApp} app the application 40 * @param {constant} type type of controller 41 * @param {string} sessionId the session id 42 * 43 * @extends ZmController 44 */ 45 ZmSearchResultsController = function(container, app, type, sessionId) { 46 47 ZmController.apply(this, arguments); 48 49 this._initialize(); 50 }; 51 52 ZmSearchResultsController.prototype = new ZmController; 53 ZmSearchResultsController.prototype.constructor = ZmSearchController; 54 55 ZmSearchResultsController.prototype.isZmSearchResultsController = true; 56 ZmSearchResultsController.prototype.toString = function() { return "ZmSearchResultsController"; }; 57 58 ZmSearchResultsController.DEFAULT_TAB_TEXT = ZmMsg.search; 59 60 ZmSearchResultsController.getDefaultViewType = 61 function() { 62 return ZmId.VIEW_SEARCH_RESULTS; 63 }; 64 ZmSearchResultsController.prototype.getDefaultViewType = ZmSearchResultsController.getDefaultViewType; 65 66 /** 67 * Displays the given results in a search tab managed by this controller. 68 * 69 * @param {ZmSearchResults} results search results 70 */ 71 ZmSearchResultsController.prototype.show = 72 function(results, resultsCtlr) { 73 var resultsType = results.type; 74 results.search.sessionId = this.sessionId; // in case we reuse this search (eg view switch) 75 var app = this._resultsApp = appCtxt.getApp(ZmItem.APP[resultsType]) || appCtxt.getCurrentApp(); 76 if (!resultsCtlr) { 77 app.showSearchResults(results, this._displayResults.bind(this, results.search), this); 78 } 79 else { 80 this._displayResults(results.search, resultsCtlr); 81 } 82 appCtxt.searchAppName = app.getName(); 83 this._curSearch = results.search; 84 this.inactive = true; // search tabs can always be reused (unless pinned) 85 }; 86 87 /** 88 * Shows the overview or the filter panel, and the mini-calendar. The overview is shown during a DnD operation. 89 * 90 * @param {Boolean} show if true, show the overview; if false, show the filter panel 91 */ 92 ZmSearchResultsController.prototype.showOverview = 93 function(show) { 94 95 var overview = this._resultsApp.getOverview(), 96 avm = appCtxt.getAppViewMgr(); 97 98 if (overview) { 99 var treeComp = {}; 100 treeComp[ZmAppViewMgr.C_TREE] = show ? overview : this._filterPanel; 101 avm.setViewComponents(this.viewId, treeComp, true); 102 avm.displayComponent(ZmAppViewMgr.C_TREE_FOOTER, show, true); 103 } 104 }; 105 106 // creates the toolbar and filter panel 107 ZmSearchResultsController.prototype._initialize = 108 function() { 109 110 this._toolbar = new ZmSearchResultsToolBar({ 111 parent: this._container, 112 controller: this, 113 id: DwtId.makeId(ZmId.SEARCHRESULTS_TOOLBAR, this._currentViewId), 114 noMenuButton: true 115 }); 116 this._toolbar.getButton(ZmSearchToolBar.SEARCH_BUTTON).addSelectionListener(this._searchListener.bind(this)); 117 var saveButton = this._toolbar.getButton(ZmSearchToolBar.SAVE_BUTTON); 118 if (saveButton) { 119 saveButton.addSelectionListener(this._saveListener.bind(this)); 120 } 121 this._toolbar.registerEnterCallback(this._searchListener.bind(this)); 122 123 this.isPinned = false; 124 }; 125 126 /** 127 * Shows the results of the given search in this controller's search tab. The toolbar and filter panel 128 * were created earlier, and the content area (top toolbar and list view) is taken from the controller 129 * that generated the results. 130 * 131 * @param {ZmSearch} search search object 132 * @param {ZmController} resultsCtlr passed back from app 133 * @private 134 */ 135 ZmSearchResultsController.prototype._displayResults = 136 function(search, resultsCtlr) { 137 138 var resultsApp = resultsCtlr.getApp().getName(); 139 if (!this._filterPanel || this._filterPanel._resultsApp !== resultsApp) { 140 this._filterPanel = new ZmSearchResultsFilterPanel({ 141 parent: this._container, 142 controller: this, 143 id: DwtId.makeId(ZmId.SEARCHRESULTS_PANEL, this._currentViewId), 144 resultsApp: resultsApp 145 }); 146 } 147 148 this._resultsController = resultsCtlr; 149 150 var elements = {}; 151 elements[ZmAppViewMgr.C_TREE] = this._filterPanel; 152 elements[ZmAppViewMgr.C_TOOLBAR_TOP] = resultsCtlr.getCurrentToolbar(); 153 elements[ZmAppViewMgr.C_APP_CONTENT] = resultsCtlr.getViewMgr ? resultsCtlr.getViewMgr() : resultsCtlr.getCurrentView(); 154 155 if (appCtxt.getCurrentViewId().indexOf(this._currentViewId) !== -1) { 156 appCtxt.getAppViewMgr().setViewComponents(this._currentViewId, elements, true); 157 } 158 else { 159 160 var callbacks = {}; 161 callbacks[ZmAppViewMgr.CB_POST_REMOVE] = this._postRemoveCallback.bind(this); 162 callbacks[ZmAppViewMgr.CB_POST_SHOW] = this._postShowCallback.bind(this); 163 elements[ZmAppViewMgr.C_SEARCH_RESULTS_TOOLBAR] = this._toolbar; 164 165 this._app.createView({ viewId: this._currentViewId, 166 viewType: this._currentViewType, 167 elements: elements, 168 callbacks: callbacks, 169 controller: this, 170 hide: [ ZmAppViewMgr.C_TREE_FOOTER ], 171 tabParams: this._getTabParams()}); 172 this._app.pushView(this._currentViewId); 173 this._filterPanel.reset(); 174 175 if (!this._button) { 176 this._button = appCtxt.getAppChooser().getButton(this.tabId); 177 Dwt.addClass(this._button.getHtmlElement(), "SearchTabButton"); 178 this._button.addSelectionListener(this._pinnedListener.bind(this)); 179 } 180 } 181 182 if (search && search.query) { 183 this._filterPanel.resetBasicFiltersToQuery(search.query); 184 } 185 186 if (search && search.origin == ZmId.SEARCH) { 187 this._toolbar.setSearch(search); 188 } 189 190 // Tell the user how many results were found 191 var searchResult = resultsCtlr.getCurrentSearchResults && resultsCtlr.getCurrentSearchResults(); 192 var results = (searchResult && searchResult.getResults()) || resultsCtlr.getList(); 193 var size = results && results.size && results.size(); 194 var plus = (results && results.hasMore && results.hasMore()) ? "+" : ""; 195 var label = size ? AjxMessageFormat.format(ZmMsg.searchResultsLabel, [size, plus]) : 196 search.isEmpty ? ZmMsg.searchResultsEnterSearch : ZmMsg.searchResultsLabelNone; 197 this._toolbar.setLabel(label, false); 198 if (resultsCtlr && resultsCtlr.updateTimeIndicator) { 199 resultsCtlr.updateTimeIndicator(); 200 } 201 setTimeout(this._toolbar.focus.bind(this._toolbar), 100); 202 }; 203 204 ZmSearchResultsController.prototype._postHideCallback = 205 function() { 206 }; 207 208 ZmSearchResultsController.prototype._postRemoveCallback = 209 function() { 210 this._app.deleteSessionController({ 211 appName: this._resultsApp.getName(), 212 controllerClass: "ZmSearchResultsController", 213 sessionId: this.sessionId 214 }); 215 }; 216 217 ZmSearchResultsController.prototype._postShowCallback = 218 function() { 219 if (appCtxt.isWebClientOfflineSupported) { 220 this.getApp().resetWebClientOfflineOperations(this); 221 } 222 }; 223 224 // returns params for the search tab button 225 ZmSearchResultsController.prototype._getTabParams = 226 function() { 227 return { 228 id: this.tabId, 229 leftImage: "Pin", 230 rightImage: "CloseGray", 231 rightHoverImage: "Close", 232 text: ZmSearchResultsController.DEFAULT_TAB_TEXT, 233 textPrecedence: 90, 234 tooltip: ZmSearchResultsController.DEFAULT_TAB_TEXT, 235 style: DwtLabel.IMAGE_BOTH 236 }; 237 }; 238 239 // runs a search based on the contents of the input 240 ZmSearchResultsController.prototype._searchListener = 241 function(ev, zimletEvent) { 242 243 // add bubble if needed before running search, but don't let "bubble added" callback trigger a search 244 var toolbar = this._toolbar, element = toolbar && toolbar._searchField.getInputElement(); 245 if (element && toolbar._acList) { 246 toolbar._settingSearch = true; 247 toolbar._acList._complete(element); 248 toolbar._settingSearch = false; 249 } 250 251 var view = appCtxt.getCurrentViewId(); //this view should be the results list view. Somehow it seems to be. 252 var sortBy = view ? appCtxt.get(ZmSetting.SORTING_PREF, view) : null; // repeat the previous sort order (from same search tab only, which is this case) 253 254 // run the search 255 var query = this._toolbar.getSearchFieldValue(); 256 var params = { 257 ev: ev, 258 zimletEvent: zimletEvent || "onSearchButtonClick", 259 query: query, 260 isEmpty: !query, 261 sessionId: this.sessionId, 262 skipUpdateSearchToolbar: true, 263 origin: ZmId.SEARCHRESULTS, 264 searchFor: this._curSearch && this._curSearch.searchFor, 265 sortBy: sortBy, 266 errorCallback: this._errorCallback.bind(this) 267 }; 268 toolbar.setLabel(ZmMsg.searching); 269 appCtxt.getSearchController()._toolbarSearch(params); 270 }; 271 272 // Note the error and then eat it - we don't want to show toast or clear out results 273 ZmSearchResultsController.prototype._errorCallback = 274 function(ex) { 275 var msg = ZmCsfeException.getErrorMsg(ex.code); 276 msg = msg || ZmMsg.unknownError; 277 this._toolbar.setLabel(msg, true); 278 return true; 279 }; 280 281 // pops up a dialog to save the search 282 ZmSearchResultsController.prototype._saveListener = 283 function(ev) { 284 285 var stc = appCtxt.getOverviewController().getTreeController(ZmOrganizer.SEARCH); 286 if (!stc._newCb) { 287 stc._newCb = stc._newCallback.bind(stc); 288 } 289 290 var params = { 291 search: this._curSearch, 292 appName: this._resultsApp.getName() 293 }; 294 ZmController.showDialog(stc._getNewDialog(), stc._newCb, params); 295 }; 296 297 // toggles the pinned state of this tab 298 ZmSearchResultsController.prototype._pinnedListener = 299 function(ev) { 300 if (!Dwt.hasClass(ev.target, "ImgPin") && !Dwt.hasClass(ev.target, "ImgUnpin")) { 301 return; 302 } 303 this.isPinned = !this.isPinned; 304 var button = appCtxt.getAppChooser().getButton(this.tabId); 305 button.setImage(this.isPinned ? "Unpin" : "Pin", DwtLabel.LEFT); 306 }; 307 308 ZmSearchResultsController.prototype._closeListener = 309 function(ev) { 310 appCtxt.getAppViewMgr().popView(false, this.getCurrentViewId()); 311 }; 312 313 // adds the given term to the search as a bubble 314 ZmSearchResultsController.prototype.addSearchTerm = 315 function(term, skipNotify, addingCond) { 316 return this._toolbar.addSearchTerm(term, skipNotify, addingCond); 317 }; 318 319 // removes the bubble with the given term 320 ZmSearchResultsController.prototype.removeSearchTerm = 321 function(term, skipNotify) { 322 this._toolbar.removeSearchTerm(term, skipNotify); 323 }; 324 325 // returns a list of current search terms 326 ZmSearchResultsController.prototype.getSearchTerms = 327 function(term, skipNotify) { 328 var values = this._toolbar._searchField.getAddresses(); 329 var terms = AjxUtil.map(values, function(member, i) { 330 return new ZmSearchToken(member); 331 }); 332 return terms; 333 }; 334