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