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, 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, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  */
 27 
 28 /**
 29  * Creates an empty tag menu.
 30  * @class
 31  * This class represents a menu structure of tags that can be added to or removed
 32  * from item(s). Based on the items passed in when it renders, it presents a 
 33  * list of tags that can be added (any tag all the items don't already have), and a
 34  * list of tags that can be removed (tags that any of the items have).
 35  * <p>
 36  * Since the content is set every time it is displayed, the tag menu doesn't need
 37  * a change listener.</p>
 38  *
 39  * @param {DwtControl}	parent		the parent widget
 40  * @param {ZmController}	controller	the owning controller
 41  * 
 42  * @extends		ZmPopupMenu
 43  */
 44 ZmTagMenu = function(parent, controller) {
 45 
 46 	// create a menu (though we don't put anything in it yet) so that parent widget shows it has one
 47 	ZmPopupMenu.call(this, parent, null, parent.getHTMLElId() + "|MENU", controller);
 48 
 49 	parent.setMenu(this);
 50 	this._addHash = {};
 51 	this._removeHash = {};
 52 	this._evtMgr = new AjxEventMgr();
 53 	this._desiredState = true;
 54 	this._items = null;
 55 	this._dirty = true;
 56 
 57 	// Use a delay to make sure our slow popup operation isn't called when someone
 58 	// is just rolling over a menu item to get somewhere else.
 59 	if (parent instanceof DwtMenuItem) {
 60 		parent.setHoverDelay(ZmTagMenu._HOVER_TIME);
 61 	}
 62 };
 63 
 64 ZmTagMenu.prototype = new ZmPopupMenu;
 65 ZmTagMenu.prototype.constructor = ZmTagMenu;
 66 
 67 ZmTagMenu.KEY_TAG_EVENT		= "_tagEvent_";
 68 ZmTagMenu.KEY_TAG_ADDED		= "_tagAdded_";
 69 ZmTagMenu.MENU_ITEM_ADD_ID	= "tag_add";
 70 ZmTagMenu.MENU_ITEM_REM_ID	= "tag_remove";
 71 
 72 ZmTagMenu._HOVER_TIME = 200;
 73 
 74 ZmTagMenu.prototype.toString =
 75 function() {
 76 	return "ZmTagMenu";
 77 };
 78 
 79 ZmTagMenu.prototype.addSelectionListener = 
 80 function(listener) {
 81 	this._evtMgr.addListener(DwtEvent.SELECTION, listener);
 82 };
 83 
 84 ZmTagMenu.prototype.removeSelectionListener = 
 85 function(listener) {
 86 	this._evtMgr.removeListener(DwtEvent.SELECTION, listener);    	
 87 };
 88 
 89 ZmTagMenu.prototype.setEnabled =
 90 function(enabled) {
 91 	// If there are no tags, then enable later
 92 	this._desiredState = enabled;
 93 	if (enabled && !this._tagList) { return; }
 94 
 95 	this.parent.setEnabled(enabled);
 96 };
 97 
 98 // Dynamically set the list of tags that can be added/removed based on the given list of items.
 99 ZmTagMenu.prototype.set =
100 function(items, tagList) {
101 	DBG.println(AjxDebug.DBG3, "set tag menu");
102 	this._tagList = tagList;
103 	this._items = items;
104 	this._dirty = true;
105 
106 	//commented out since in ZmMailMsgCapsuleView.prototype._resetOperations we call resetOperations of the ctrlr before this set. And I don't think this should enable the button anyway - this should be done elsewhere like it is.
107 	//another option would have been to reorder but I think this one is the safer one.
108 	//this.parent.setEnabled(true);
109 
110 	// Turn on the hover delay.
111 	if (this.parent instanceof DwtMenuItem) {
112 		this.parent.setHoverDelay(ZmTagMenu._HOVER_TIME);
113 	}
114 };
115 
116 ZmTagMenu.prototype._doPopup =
117 function(x, y, kbGenerated) {
118 	if (this._dirty) {
119 		// reset the menu
120 		this.removeChildren();
121 
122 		if (this._tagList) {
123 			var rootTag = this._tagList.root;
124 			var addRemove = this._getAddRemove(this._items, rootTag);
125 			this._render(rootTag, addRemove);
126 		}
127 		this._dirty = false;
128 
129 		// Remove the hover delay to prevent flicker when mousing around.
130 		if (this.parent instanceof DwtMenuItem) {
131 			this.parent.setHoverDelay(0);
132 		}
133 	}
134 	ZmPopupMenu.prototype._doPopup.call(this, x, y, kbGenerated);
135 };
136 
137 
138 // Given a list of items, produce two lists: one of tags that could be added (any tag
139 // that the entire list doesn't have), and one of tags that could be removed (any tag
140 // that any item has).
141 ZmTagMenu.prototype._getAddRemove = 
142 function(items, tagList) {
143 	// find out how many times each tag shows up in the items
144 	var tagCount = {};
145 	var tagRemoveHash = {};
146 	for (var i = 0; i < items.length; i++) {
147 		var item = items[i];
148 		if (!item.tags) {
149 			continue;
150 		}
151 		for (var j = 0; j < item.tags.length; j++) {
152 			var tagName = item.tags[j];
153 			tagCount[tagName] = tagCount[tagName] || 0;
154 			tagRemoveHash[tagName]  = true;
155 			//NOTE hasTag and canAddTag are not interchangeable - for Conv it's possible you can both add the tag and remove (if only some messages are tagged)
156 			if (!item.canAddTag(tagName)) {
157 				tagCount[tagName] += 1;
158 			}
159 		}
160 	}
161 	var remove = AjxUtil.keys(tagRemoveHash);
162 
163 	var add = [];
164 	// any tag held by fewer than all the items can be added
165 	var a = tagList.children.getArray();
166 	for (i = 0; i < a.length; i++) {
167 		var tag = a[i];
168 		tagName = tag.name;
169 		if (!tagCount[tagName] || (tagCount[tagName] < items.length)) {
170 			add.push(tagName);
171 		}
172 	}
173 
174 	return {add: add, remove: remove};
175 };
176 
177 // Create the list of tags that can be added, and the submenu with the list of
178 // tags that can be removed.
179 ZmTagMenu.prototype._render =
180 function(tagList, addRemove) {
181 
182 	for (var i = 0; i < addRemove.add.length; i++) {
183 		var tagName = addRemove.add[i];
184 		this._addNewTag(this, tagName, tagList, true, null, this._addHash);
185 	}
186 
187 	if (addRemove.add.length) {
188 		new DwtMenuItem({parent:this, style:DwtMenuItem.SEPARATOR_STYLE});
189 	}
190 
191 	// add static "New Tag" menu item
192 	var map = appCtxt.getCurrentController() && appCtxt.getCurrentController().getKeyMapName();
193 	var addid = map ? (map + "_newtag"):this._htmlElId + "|NEWTAG";
194 	var removeid = map ? (map + "_removetag"):this._htmlElId + "|REMOVETAG";
195 
196 	var miNew = this._menuItems[ZmTagMenu.MENU_ITEM_ADD_ID] = new DwtMenuItem({parent:this, id: addid});
197 	miNew.setText(AjxStringUtil.htmlEncode(ZmMsg.newTag));
198 	miNew.setImage("NewTag");
199 	miNew.setShortcut(appCtxt.getShortcutHint(this._keyMap, ZmKeyMap.NEW_TAG));
200 	miNew.setData(ZmTagMenu.KEY_TAG_EVENT, ZmEvent.E_CREATE);
201 	miNew.addSelectionListener(new AjxListener(this, this._menuItemSelectionListener), 0);
202 	miNew.setEnabled(!appCtxt.isWebClientOffline());
203 
204 	// add static "Remove Tag" menu item
205 	var miRemove = this._menuItems[ZmTagMenu.MENU_ITEM_REM_ID] = new DwtMenuItem({parent:this, id: removeid});
206 	miRemove.setEnabled(false);
207 	miRemove.setText(AjxStringUtil.htmlEncode(ZmMsg.removeTag));
208 	miRemove.setImage("DeleteTag");
209 
210 	var removeList = addRemove.remove;
211 	if (removeList.length > 0) {
212 		miRemove.setEnabled(true);
213 		var removeMenu = null;
214 		if (removeList.length > 1) {
215 			for (i = 0; i < removeList.length; i++) {
216 				if (!removeMenu) {
217 					removeMenu = new DwtMenu({parent:miRemove, className:this._className});
218 					miRemove.setMenu(removeMenu);
219                     removeMenu.setHtmlElementId('REMOVE_TAG_MENU_' + this.getHTMLElId());
220 				}
221 				var tagName = removeList[i];
222                 var tagHtmlId = 'Remove_tag_' + i;
223 				this._addNewTag(removeMenu, tagName, tagList, false, null, this._removeHash, tagHtmlId);
224 			}
225 			// if multiple removable tags, offer "Remove All"
226 			new DwtMenuItem({parent:removeMenu, style:DwtMenuItem.SEPARATOR_STYLE});
227 			var mi = new DwtMenuItem({parent:removeMenu, id:"REMOVE_ALL_TAGS"});
228 			mi.setText(ZmMsg.allTags);
229 			mi.setImage("TagStack");
230 			mi.setShortcut(appCtxt.getShortcutHint(this._keyMap, ZmKeyMap.UNTAG));
231 			mi.setData(ZmTagMenu.KEY_TAG_EVENT, ZmEvent.E_REMOVE_ALL);
232 			mi.setData(Dwt.KEY_OBJECT, removeList);
233 			mi.addSelectionListener(new AjxListener(this, this._menuItemSelectionListener), 0);
234 		}
235 		else {
236 			var tag = tagList.getByNameOrRemote(removeList[0]);
237 			miRemove.setData(ZmTagMenu.KEY_TAG_EVENT, ZmEvent.E_TAGS);
238 			miRemove.setData(ZmTagMenu.KEY_TAG_ADDED, false);
239 			miRemove.setData(Dwt.KEY_OBJECT, tag);
240 			miRemove.addSelectionListener(new AjxListener(this, this._menuItemSelectionListener), 0);
241 		}		
242 
243 	}
244 };
245 
246 ZmTagMenu.tagNameLength = 20;
247 ZmTagMenu.prototype._addNewTag =
248 function(menu, newTagName, tagList, add, index, tagHash, tagHtmlId) {
249 	var newTag = tagList.getByNameOrRemote(newTagName);
250 	var mi = new DwtMenuItem({parent:menu, index:index, id:tagHtmlId});
251     var tagName = AjxStringUtil.clipByLength(newTag.getName(false),ZmTagMenu.tagNameLength);
252 	var nameText = newTag.notLocal ? AjxMessageFormat.format(ZmMsg.tagNotLocal, tagName) : tagName;
253     mi.setText(nameText);
254     mi.setImage(newTag.getIconWithColor());
255 	mi.setData(ZmTagMenu.KEY_TAG_EVENT, ZmEvent.E_TAGS);
256 	mi.setData(ZmTagMenu.KEY_TAG_ADDED, add);
257 	mi.setData(Dwt.KEY_OBJECT, newTag);
258 	mi.addSelectionListener(new AjxListener(this, this._menuItemSelectionListener), 0);
259 //	mi.setShortcut(appCtxt.getShortcutHint(null, ZmKeyMap.TAG));
260 	tagHash[newTag.id] = mi;
261 };
262 
263 ZmTagMenu.prototype._menuItemSelectionListener =
264 function(ev) {
265 	// Only notify if the node is one of our nodes
266 	if (ev.item.getData(ZmTagMenu.KEY_TAG_EVENT)) {
267 		this._evtMgr.notifyListeners(DwtEvent.SELECTION, ev);
268 	}
269 };
270 
271