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