1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2011, 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, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @class
 26  * This is a folder choosing widget designed to be displayed as a dropdown hanging off a menu. Other than
 27  * that, it works mostly like ZmChooseFolderDialog. Instead of a New button, it has a "New Folder" menu item
 28  * at the bottom. Items are moved when a menu item is clicked, instead of the OK button in the dialog.
 29  * 
 30  * The implementation mostly relies on calling ZmChooseFolderDialog and ZmDialog methods, since those were
 31  * already written. A cleaner implementation would probably include a base widget that this and the dialog
 32  * would use.
 33  *
 34  * @author Eran Yarkon
 35  *
 36  * @param {hash}		params			a hash of parameters
 37  * @param {DwtComposite}      params.parent			the parent widget
 38  * @param {string}      params.className			the CSS class
 39  * @param {constant}      params.posStyle			the positioning style (see {@link Dwt})
 40  *
 41  * @extends		DwtComposite
 42  */
 43 ZmFolderChooser = function(params) {
 44 	if (arguments.length == 0) { return; }
 45 	params.className = params.className || "ZmFolderChooser";
 46 	DwtComposite.call(this, params);
 47 
 48 	this._overview = {};
 49 	this._opc = appCtxt.getOverviewController();
 50 	this._treeView = {};
 51 	this._folderTreeDivId = this._htmlElId + "_folderTreeDivId";
 52 
 53 	this._uuid = Dwt.getNextId();
 54 
 55 	AjxDispatcher.require("Extras");	// ZmChooseFolderDialog
 56 	this._changeListener = ZmChooseFolderDialog.prototype._folderTreeChangeListener.bind(this);
 57 	this._treeViewListener = this._treeViewSelectionListener.bind(this);
 58 
 59 	var moveMenu = params.parent;
 60 	moveMenu._addItem(this, params.index); //this is what DwtMenuItem does. Allows this item to be in the menu items table - better for layout purposes such as consistent widths
 61 	this._moveOnFolderCreate = true;
 62 	
 63 	if (!params.hideNewButton) {
 64 		//add separator menu item on the move menu (the parent)
 65 		new DwtMenuItem({parent:moveMenu, style:DwtMenuItem.SEPARATOR_STYLE});
 66 	
 67 		// add New button
 68 		var newFolderItem = this._newButton = new DwtMenuItem({parent:moveMenu, id: moveMenu.getHTMLElId() + "|NEWFOLDER"});
 69 		var appName = appCtxt.getCurrentAppName();	
 70 		var defaultApp = ZmApp.MAIL;
 71 		var newTextKey = ZmFolderChooser.NEW_ORG_KEY[appName] || ZmFolderChooser.NEW_ORG_KEY[defaultApp];
 72 		var newImage = ZmFolderChooser.NEW_ORG_ICON[appName] || ZmFolderChooser.NEW_ORG_ICON[defaultApp];
 73 		var newShortcut = ZmFolderChooser.NEW_ORG_SHORTCUT[appName];
 74 		newFolderItem.setText(ZmMsg[newTextKey]);
 75 		newFolderItem.setImage(newImage);
 76 		if (newShortcut) {
 77 			newFolderItem.setShortcut(appCtxt.getShortcutHint(this._keyMap, newShortcut));
 78 		}
 79 		newFolderItem.addSelectionListener(this._showNewDialog.bind(this));
 80 	}
 81 
 82 	this._init();
 83 };
 84 
 85 ZmFolderChooser.prototype = new DwtComposite;
 86 ZmFolderChooser.prototype.constructor = ZmFolderChooser;
 87 
 88 ZmFolderChooser.prototype.isZmFolderChooser = true;
 89 ZmFolderChooser.prototype.toString = function() { return "ZmFolderChooser"; };
 90 
 91 
 92 // Properties for New button at bottom
 93 ZmFolderChooser.NEW_ORG_KEY = {};
 94 ZmFolderChooser.NEW_ORG_KEY[ZmApp.MAIL]				= "newFolder";
 95 ZmFolderChooser.NEW_ORG_KEY[ZmApp.CONTACTS]			= "newAddrBook";
 96 ZmFolderChooser.NEW_ORG_KEY[ZmApp.CALENDAR]			= "newCalendar";
 97 ZmFolderChooser.NEW_ORG_KEY[ZmApp.TASKS]			= "newTaskFolder";
 98 
 99 ZmFolderChooser.NEW_ORG_ICON = {};
100 ZmFolderChooser.NEW_ORG_ICON[ZmApp.MAIL]			= "NewFolder";
101 ZmFolderChooser.NEW_ORG_ICON[ZmApp.CONTACTS]		= "NewContactsFolder";
102 ZmFolderChooser.NEW_ORG_ICON[ZmApp.CALENDAR]		= "NewAppointment";
103 ZmFolderChooser.NEW_ORG_ICON[ZmApp.TASKS]			= "NewTaskList";
104 
105 ZmFolderChooser.NEW_ORG_SHORTCUT = {};
106 ZmFolderChooser.NEW_ORG_SHORTCUT[ZmApp.MAIL]		= ZmKeyMap.NEW_FOLDER;
107 ZmFolderChooser.NEW_ORG_SHORTCUT[ZmApp.CALENDAR]	= ZmKeyMap.NEW_CALENDAR;
108 
109 /**
110  *
111  * see ZmChooseFolderDialog.prototype.popup
112  */
113 ZmFolderChooser.prototype.setupFolderChooser =
114 function(params, selectionCallback) {
115 
116 	this._selectionCallback = selectionCallback;
117 	this._overviewId = params.overviewId;
118 
119 	ZmChooseFolderDialog.prototype.popup.call(this, params, true);
120 };
121 
122 ZmFolderChooser.prototype._getNewButton =
123 function () {
124 	return this._newButton;
125 };
126 
127 ZmFolderChooser.prototype.updateData =
128 function(data) {
129 	this._data = data;
130 };
131 
132 ZmFolderChooser.prototype._focus =
133 function() {
134 	var overview = this._overview[this._overviewId];
135 	if (overview) {
136 		overview.focus();
137 	}
138 };
139 
140 /**
141  * this is not really doing the popup, just setting more stuff up, but to reuse the caller (ZmChooseFolderDialog.prototype.popup)
142  * from ZmChooseFolderDialog, I had to keep the name.
143  *
144  * @param params
145  */
146 ZmFolderChooser.prototype._doPopup =
147 function(params) {
148 	ZmChooseFolderDialog.prototype._doPopup.call(this, params, true);
149 };
150 
151 
152 /**
153  * this reuses ZmDialog stuff. With slight necessary changes. Might be fragile if this is changed in ZmDialog
154  * in which case we might be better off with copy-paste. but for now it works.
155  * 
156  * @param params
157  * @param forceSingle
158  */
159 ZmFolderChooser.prototype._setOverview =
160 function(params, forceSingle) {
161 	params.overviewClass = "menuOverview";
162 	params.dynamicWidth = true;
163 
164 	var overview = ZmDialog.prototype._setOverview.call(this, params, forceSingle); //reuse from ZmDialog
165 
166 	if (!appCtxt.multiAccounts || forceSingle) {
167 		//this  is needed for some reason
168 		this._overview[params.overviewId] = overview;
169 	}
170 
171 	return overview;
172 };
173 
174 /**
175  * delegate to ZmDialog. called from ZmDialog.prototype._setOverview (which we delegate to from ZmFolderChooser.prototype._setOverview)
176  */
177 ZmFolderChooser.prototype._renderOverview =
178 function() {
179 	ZmDialog.prototype._renderOverview.apply(this, arguments); //reuse code from ZmDialog
180 };
181 
182 /**
183  * delegate to ZmDialog.
184  */
185 ZmFolderChooser.prototype._setRootSelection =
186 function() {
187 	ZmDialog.prototype._setRootSelection.apply(this, arguments); //reuse code from ZmDialog
188 };
189 
190 
191 /**
192  * delegate to ZmDialog. called from ZmDialog.prototype._setOverview (which we delegate to from ZmFolderChooser.prototype._setOverview)
193  */
194 ZmFolderChooser.prototype._makeOverviewVisible =
195 function() {
196 	ZmDialog.prototype._makeOverviewVisible.apply(this, arguments); //reuse code from ZmDialog
197 };
198 
199 ZmFolderChooser.prototype._resetTree =
200 function(treeIds, overview) {
201 	ZmChooseFolderDialog.prototype._resetTree.call(this, treeIds, overview);
202 };
203 
204 ZmFolderChooser.prototype._getOverview =
205 function() {
206 	return ZmChooseFolderDialog.prototype._getOverview.call(this)
207 };
208 
209 ZmFolderChooser.prototype._treeViewSelectionListener =
210 function(ev) {
211 	if (ev.detail != DwtTree.ITEM_SELECTED) {
212 		return;
213 	}
214 	//set in DwtTree.prototype._itemClicked and DwtTree.prototype.setSelection (which is called by DwtTreeItem.prototype.handleKeyAction)
215 	if (!ev.clicked && !ev.enter) {
216 		return;
217 	}
218 
219 	//I kept this logic from ZmChooseFolderDialog.prototype._treeViewSelectionListener. Not sure what it means exactly
220 	if (this._getOverview() instanceof ZmAccountOverviewContainer) {
221 		if (ev.item instanceof DwtHeaderTreeItem) {
222 			return;
223 		}
224 
225 		var oc = this._opc.getOverviewContainer(this._curOverviewId);
226 		var overview = oc.getOverview(ev.item.getData(ZmTreeView.KEY_ID));
227 		oc.deselectAll(overview);
228 	}
229 
230 	var organizer = ev.item && ev.item.getData(Dwt.KEY_OBJECT);
231 	if (organizer.id == ZmFolder.ID_LOAD_FOLDERS) {
232 		return; // user clicked on "Show More Folders", it's not a real selection, it just expanded more folders.
233 	}
234 	var value = organizer ? organizer.getName(null, null, true) : ev.item.getText();
235 	this._lastVal = value.toLowerCase();
236 	this._doSelection();
237 };
238 
239 /**
240  * copied mostly from ZmChooseFolderDialog.prototype._okButtonListener  
241  * @param tgtFolder
242  */
243 ZmFolderChooser.prototype._doSelection =
244 function(tgtFolder) {
245     tgtFolder = tgtFolder || this._getOverview().getSelected();
246     if  (!tgtFolder) {
247         tgtFolder = appCtxt.getById(this._selected);
248     }
249 	var folderList = (tgtFolder && (!(tgtFolder instanceof Array)))
250 		? [tgtFolder] : tgtFolder;
251 
252 	var msg = (!folderList || (folderList && folderList.length == 0))
253 		? ZmMsg.noTargetFolder : null;
254 
255 	//todo - what is that? can you move stuff to multiple targets?  annotation on ZmChooseFolderDialog show it might be for filters on multiple folders. obviously in that case we can't have a drop down. we might have to keep that folder dialog
256 	
257 	// check for valid target
258 	if (!msg && this._data) {
259 		for (var i = 0; i < folderList.length; i++) {
260 			var folder = folderList[i];
261 			if (folder.mayContain && !folder.mayContain(this._data, null, this._acceptFolderMatch)) {
262 				if (this._data instanceof ZmFolder) {
263 					msg = ZmMsg.badTargetFolder;
264 				}
265 				else {
266 					var items = AjxUtil.toArray(this._data);
267 					for (var i = 0; i < items.length; i++) {
268 						var item = items[i];
269 						if (!item) {
270 							continue;
271 						}
272 						if (item.isDraft && (folder.nId != ZmFolder.ID_TRASH && folder.nId != ZmFolder.ID_DRAFTS && folder.rid != ZmFolder.ID_DRAFTS)) {
273 							// can move drafts into Trash or Drafts
274 							msg = ZmMsg.badTargetFolderForDraftItem;
275 							break;
276 						}
277 						else if ((folder.nId == ZmFolder.ID_DRAFTS || folder.rid == ZmFolder.ID_DRAFTS) && !item.isDraft)	{
278 							// only drafts can be moved into Drafts
279 							msg = ZmMsg.badItemForDraftsFolder;
280 							break;
281 						}
282 					}
283 					if (!msg) {
284 						msg = ZmMsg.badTargetFolderItems;
285 					}
286 				}
287 				break;
288 			}
289 		}
290 	}
291 
292 	if (msg) {
293 		ZmDialog.prototype._showError.call(this, msg);
294 		return;
295 	}
296 	if (this._selectionCallback) {
297 		this._selectionCallback(tgtFolder);
298 	}
299 };
300 
301 ZmFolderChooser.prototype._resetTreeView =
302 function(visible) {
303 	ZmChooseFolderDialog.prototype._resetTreeView.call(this, visible);
304 };
305 
306 ZmFolderChooser.prototype.getOverviewId =
307 function(part) {
308 	return appCtxt.getOverviewId([this.toString(), part], null);
309 };
310 
311 ZmFolderChooser.prototype._loadFolders =
312 function() {
313 	ZmChooseFolderDialog.prototype._loadFolders.call(this);
314 };
315 
316 ZmFolderChooser.prototype._init =
317 function() {
318 
319 	var html = [], idx = 0;
320 
321 	html[idx++] =	"<table cellspacing='0' cellpadding='0' style='border-collapse:collapse;'>";
322 	html[idx++] =		"<tr><td><div id='" + this._folderTreeDivId + "'>";
323 	html[idx++] =		"</div></td></tr>";
324 	html[idx++] =	"</table>";
325 
326 	this.getHtmlElement().innerHTML = html.join("");
327 };
328 
329 ZmFolderChooser.prototype._showNewDialog =
330 function() {
331 	var item = this._getOverview().getSelected(true);
332 	var newType = (item && item.type) || this._treeIds[0];
333 	var ftc = this._opc.getTreeController(newType);
334 	var dialog = ftc._getNewDialog();
335 	dialog.reset();
336 	dialog.registerCallback(DwtDialog.OK_BUTTON, ZmChooseFolderDialog.prototype._newCallback, this, [ftc, dialog]);
337 	this.parent.popdown(); //pop it down so it doenst pop down when user clicks on the "new" dialog, confusing them. this is also consistent with the tag menu "new".
338 	dialog.popup();
339 };
340