1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 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 new autocomplete list.
 31  * @class
 32  * This class shows the members of an expanded distribution list (DL).
 33  *
 34  * @author Conrad Damon
 35  *
 36  * @param {Hash}	params			a hash of parameters:
 37  * @param	{ZmAutocompleteListView}		parent autocomplete list view
 38  * @param	{AjxCallback}	selectionCallback	the callback into client to notify it that selection from extended DL happened (passed from email.js, and accessed from ZmDLAutocompleteListView.prototype._doUpdate)
 39  *
 40  * @extends		ZmAutocompleteListView
 41  */
 42 ZmDLAutocompleteListView = function(params) {
 43 	params.isFocusable = true;
 44 	ZmAutocompleteListView.call(this, params);
 45 	this._parentAclv = params.parentAclv;
 46 	this._dlScrollDiv = this.getHtmlElement();
 47 	this._selectionCallback = params.selectionCallback;
 48 	this._expandTextId = params.expandTextId;
 49 	Dwt.setHandler(this._dlScrollDiv, DwtEvent.ONSCROLL, ZmDLAutocompleteListView.handleDLScroll);
 50 };
 51 
 52 ZmDLAutocompleteListView.prototype = new ZmAutocompleteListView;
 53 ZmDLAutocompleteListView.prototype.constructor = ZmDLAutocompleteListView;
 54 
 55 ZmDLAutocompleteListView.prototype.toString =
 56 function() {
 57 	return "ZmDLAutocompleteListView";
 58 };
 59 
 60 
 61 ZmDLAutocompleteListView.prototype.getKeyMapName = function() {
 62 	return ZmKeyMap.MAP_DL_ADDRESS_LIST;
 63 };
 64 
 65 ZmDLAutocompleteListView.prototype.handleKeyAction = function(actionCode, ev) {
 66 	DBG.println("aif", "handle shortcut: " + actionCode);
 67 
 68 	switch (actionCode) {
 69 		case DwtKeyMap.SELECT_NEXT:	this._setSelected(ZmAutocompleteListView.NEXT); break;
 70 		case DwtKeyMap.SELECT_PREV:	this._setSelected(ZmAutocompleteListView.PREV); break;
 71 		case DwtKeyMap.ENTER:		this._update();  break;
 72 		case DwtKeyMap.CANCEL:		if (this._parentAclv && this._expandTextId) {
 73 										this._parentAclv._setExpandText(this._expandTextId, false);
 74 									}
 75 									this._popdown();
 76 									break;
 77 		default: return false;
 78 	}
 79 	return true;
 80 };
 81 
 82 
 83 ZmDLAutocompleteListView.prototype._set =
 84 function(list, contact) {
 85 
 86 	this._removeAll();
 87 	this._matches = [];
 88 	this._addMembers(list);
 89 
 90 	// add row for selecting all at top of list
 91 	var dl = appCtxt.getApp(ZmApp.CONTACTS).getDL(contact.getEmail());
 92 	var numMembers = dl ? dl.total : list.length;
 93 	var selectId = this._getId("Row", 1);
 94 	if (numMembers != 1) {
 95 		var table = this._getTable();
 96 		var row = table.insertRow(0);
 97 		row.className = this._origClass;
 98 		selectId = row.id = this._selectAllRowId = this._getId("Row", "selectAll");
 99 		var cell = row.insertCell(-1);
100 		cell.className = "AutocompleteMatchIcon";
101 		cell.innerHTML = AjxImg.getImageHtml("Blank16");
102 		cell = row.insertCell(-1);
103 		var text = numMembers ? ZmMsg.selectAllMembers : ZmMsg.noMembers;
104 		cell.innerHTML = AjxMessageFormat.format(text, [numMembers]);
105 	}
106 
107 	AjxTimedAction.scheduleAction(new AjxTimedAction(this,
108 		function() {
109 			this._setSelected(selectId);
110 		}), 100);
111 };
112 
113 ZmDLAutocompleteListView.prototype._addMembers =
114 function(list) {
115 
116 	var table = this._getTable();
117 	var len = list.length;
118 	for (var i = 0; i < len; i++) {
119 		var match = list[i];
120 		this._matches.push(match);
121 		var rowId = match.id = this._getId("Row", this._matches.length);
122 		this._addRow(table, match, rowId);
123 	}
124 };
125 
126 ZmDLAutocompleteListView.prototype._addRow =
127 function(table, match, rowId) {
128 
129 	if (match && (match.text || match.icon)) {
130 		this._matchHash[rowId] = match;
131 		var row = table.insertRow(-1);
132 		row.className = this._origClass;
133 		row.id = rowId;
134 		var cell = row.insertCell(-1);
135 		cell.className = "AutocompleteMatchIcon";
136 		if (match.icon) {
137 			cell.innerHTML = (match.icon.indexOf('Dwt') != -1) ? ["<div class='", match.icon, "'></div>"].join("") :
138 																  AjxImg.getImageHtml(match.icon);
139 		} else {
140 			cell.innerHTML = " ";
141 		}
142 		cell = row.insertCell(-1);
143 		cell.innerHTML = match.text || " ";
144 	}
145 };
146 
147 ZmDLAutocompleteListView.prototype._update =
148 function(context, match, ev) {
149 	
150 	if (this._selected == this._selectAllRowId) {
151 		if (!this._matchHash[this._selectAllRowId]) {
152 			var callback = this._handleResponseGetAllDLMembers.bind(this, ev);
153 			this._dlContact.getAllDLMembers(callback);
154 		}
155 	} else {
156 		this._doUpdate();
157 		this.reset(true);
158 	}
159 };
160 
161 ZmDLAutocompleteListView.prototype._handleResponseGetAllDLMembers =
162 function(ev, result) {
163 
164 	var mv = this._parentAclv._matchValue;
165 	var field = (mv instanceof Array) ? mv[0] : mv;
166 	if (result.list && result.list.length) {
167 		// see if client wants addresses joined, or one at a time
168 		if (this._parentAclv._options.massDLComplete) {
169 			var match = this._matchHash[this._selectAllRowId] = new ZmAutocompleteMatch();
170 			match[field] = result.list.join(this._parentAclv._separator);
171 			match.multipleAddresses = true;
172 			this._doUpdate();
173 		}
174 		else {
175 			var match = new ZmAutocompleteMatch();
176 			for (var i = 0, len = result.list.length; i < len; i++) {
177 				match[field] = result.list[i];
178 				this._doUpdate(match);
179 			}
180 		}
181 	}
182 	this.reset(true);
183 };
184 
185 ZmDLAutocompleteListView.prototype._doUpdate =
186 function(match) {
187 
188 	var context = null;
189 	// so that address will be taken from match
190 	if (this._parentAclv && this._parentAclv._currentContext) {
191 		context = this._parentAclv._currentContext;
192 		context.address = null;
193 	}
194 	match = match || this._matchHash[this._selected];
195 	if (!match) {
196 		return;
197 	}
198 
199 	if (this._selectionCallback) {
200 		this._selectionCallback(match.fullAddress);
201 		return;
202 	}
203 
204 	var dlBubble = document.getElementById(this._dlBubbleId);
205 	if (dlBubble && dlBubble._aifId && (!context || context.element._aifId != dlBubble._aifId)) {
206 		//this is the special case the DL was pre-created with the view. In this case we might have no context.
207 		// Another possible bug this fixes is if the current context is not in the same input field as the DL we are selecting from.
208 		var addrInputFld = DwtControl.ALL_BY_ID[dlBubble._aifId];
209 		if (addrInputFld){ 
210 			var bubbleParams = {
211 				address:	match.fullAddress,
212 				match:		match,
213 				noFocus:	false,
214 				addClass:	null,
215 				noParse:	false
216 			};
217 			addrInputFld.addBubble(bubbleParams);
218 			return;
219 		}
220 	}
221 
222 	this._parentAclv._update(null, match);
223 };
224 
225 ZmDLAutocompleteListView.handleDLScroll =
226 function(ev) {
227 
228 	var target = DwtUiEvent.getTarget(ev);
229 	var view = DwtControl.findControl(target);
230 	var div = view._dlScrollDiv;
231 	if (div.clientHeight == div.scrollHeight) { return; }
232 	var contactDL = appCtxt.getApp(ZmApp.CONTACTS).getDL(view._dlContact.getEmail());
233 	var listSize = view.getDLSize();
234 	if (contactDL && (contactDL.more || (listSize < contactDL.list.length))) {
235 		var params = {scrollDiv:	div,
236 					  rowHeight:	view._rowHeight,
237 					  threshold:	10,
238 					  limit:		ZmContact.DL_PAGE_SIZE,
239 					  listSize:		listSize};
240 		var needed = ZmListView.getRowsNeeded(params);
241 		DBG.println("dl", "scroll, items needed: " + needed);
242 		if (needed) {
243 			DBG.println("dl", "new offset: " + listSize);
244 			var respCallback = ZmDLAutocompleteListView._handleResponseDLScroll.bind(null, view);
245 			view._parentAclv._dataAPI.expandDL(view._dlContact, listSize, respCallback);
246 		}
247 	}
248 };
249 
250 ZmDLAutocompleteListView._handleResponseDLScroll =
251 function(view, matches) {
252 	view._addMembers(matches);
253 };
254 
255 ZmDLAutocompleteListView.prototype.getDLSize =
256 function() {
257 	return this.size() - 1;
258 };
259 
260 // optionally removes the DL address bubble
261 ZmDLAutocompleteListView.prototype.reset =
262 function(clearDL) {
263 
264 	if (clearDL) {
265 		var dlBubble = document.getElementById(this._dlBubbleId);
266 		if (dlBubble) {
267 			var addrInput = DwtControl.ALL_BY_ID[dlBubble._aifId];
268 			if (addrInput && addrInput.removeBubble) { //it's not always really addrInput - from msg/conv view it's the msg or conv view, (unlike compose view where it's really address input
269 				addrInput.removeBubble(this._dlBubbleId);
270 				this._dlBubbleId = null;
271 			}
272 		}
273 	}
274 	ZmAutocompleteListView.prototype.reset.call(this);
275 };
276 
277 ZmDLAutocompleteListView.prototype._popup =
278 function(loc) {
279 
280 	if (this.getVisible()) { return; }
281 
282 	loc = loc || this._getDefaultLoc();
283 	var x = loc.x;
284 	var windowSize = this.shell.getSize();
285 	this.setVisible(true);
286 	var curSize = this.getSize();
287 	this.setVisible(false);
288 	var newX = (x + curSize.x >= windowSize.x) ? windowSize.x - curSize.x : x;
289 	if (newX != x) {
290 		var parentSize = this._parentAclv.getSize();
291 		this._parentAclv.setLocation(windowSize.x - (curSize.x + parentSize.x + 2), Dwt.DEFAULT);
292 		loc.x = newX;
293 	}
294 	ZmAutocompleteListView.prototype._popup.call(this, loc);
295 	this.focus();
296 };
297 
298 ZmDLAutocompleteListView.prototype._popdown =
299 function() {
300 	if (this._parentAclv) {
301 		this._parentAclv._curExpanded = null;
302 	}
303 	ZmAutocompleteListView.prototype._popdown.call(this);
304 };
305