1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 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) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 
 25 /**
 26  * Creates a menu item.
 27  * @constructor
 28  * @class
 29  * Menu items can be part of a radio group, or can be checked style menu items.
 30  *
 31  * @author Ross Dargahi
 32  * 
 33  * @param {hash}	params		a hash of parameters
 34  * @param  {DwtComposite}     params.parent		the parent widget
 35  * @param  {constant}     params.style			the menu item style
 36  * @param  {string}     params.radioGroupId 	the radio group that the menu item is part of
 37  * @param  {number}     params.index 			the position in menu
 38  * @param  {string}     params.className		the CSS class
 39  * @param  {constant}     params.posStyle		the positioning style (see {@link DwtControl})
 40  * @param  {string}     params.id			an explicit ID to use for the control's HTML element
 41  * 
 42  * @extends		DwtButton
 43  */
 44 DwtMenuItem = function(params) {
 45 	if (arguments.length == 0) { return; }
 46 	params = Dwt.getParams(arguments, DwtMenuItem.PARAMS);
 47 
 48 	// check parameters
 49 	var parent = params.parent;
 50 	if (!(parent && parent.isDwtMenu)) {
 51 		throw new DwtException("Parent must be a DwtMenu object", DwtException.INVALIDPARENT, "DwtMenuItem");
 52 	}
 53 
 54 	var style = params.style = params.style || DwtMenuItem.NO_STYLE;
 55 	if (parent._style == DwtMenu.BAR_STYLE && style != DwtMenuItem.PUSH_STYLE) {
 56 		throw new DwtException("DwtMenuItemInit: invalid style", DwtException.INVALID_PARAM, "DwtMenuItem");
 57 	}
 58 
 59 	// call super constructor
 60 	params.className = params.className || "ZMenuItem";
 61 	style &= ~DwtLabel.IMAGE_RIGHT; // remove image right style
 62 	style |= DwtButton.ALWAYS_FLAT | DwtLabel.IMAGE_LEFT; // set default styles
 63 	var isSeparator = (style & DwtMenuItem.SEPARATOR_STYLE);
 64 	if (isSeparator) {
 65 		params.className = "ZMenuItemSeparator";
 66 		this.isFocusable = false;
 67         this.noTab = true;
 68 		this.role = null;
 69 	}
 70 	params.listeners = DwtMenuItem._listeners;
 71 	DwtButton.call(this, params);
 72 
 73 	this.setDropDownImages("Cascade", "Cascade", "Cascade", "Cascade");
 74 	this._radioGroupId = params.radioGroupId;
 75 
 76 	// add this item at the specified index
 77 	if (parent._addItem) {
 78 		parent._addItem(this, params.index);
 79 	}
 80 
 81 	// add listeners if not menu item separator
 82 	if (!isSeparator) {
 83 		this.addSelectionListener(this.__handleItemSelect.bind(this));
 84 	}
 85 };
 86 
 87 DwtMenuItem.PARAMS = ["parent", "style", "radioGroupId", "index", "className", "posStyle", "id"];
 88 
 89 DwtMenuItem.prototype = new DwtButton;
 90 DwtMenuItem.prototype.constructor = DwtMenuItem;
 91 
 92 DwtMenuItem.prototype.isDwtMenuItem = true;
 93 DwtMenuItem.prototype.toString = function() { return "DwtMenuItem"; };
 94 DwtMenuItem.prototype.role = "menuitem";
 95 
 96 //
 97 // Constants
 98 //
 99 
100 DwtMenuItem.CHECKED			= 1;
101 DwtMenuItem.UNCHECKED		= 2;
102 
103 /**
104  * Defines the "no" style.
105  */
106 DwtMenuItem.NO_STYLE		= 0;
107 /**
108  * Defines the "check" style.
109  */
110 DwtMenuItem.CHECK_STYLE		= DwtButton._LAST_STYLE * 2;
111 /**
112  * Defines the "radio" style.
113  */
114 DwtMenuItem.RADIO_STYLE		= DwtButton._LAST_STYLE * 4;
115 /**
116  * Defines the "separator" style.
117  */
118 DwtMenuItem.SEPARATOR_STYLE = DwtButton._LAST_STYLE * 8;
119 /**
120  * Defines the "cascade" style.
121  */
122 DwtMenuItem.CASCADE_STYLE	= DwtButton._LAST_STYLE * 16;
123 /**
124  * Defines the "push" style.
125  */
126 DwtMenuItem.PUSH_STYLE		= DwtButton._LAST_STYLE * 32;
127 /**
128  * Defines the "select" style.
129  */
130 DwtMenuItem.SELECT_STYLE	= DwtButton._LAST_STYLE * 64;
131 DwtMenuItem._LAST_STYLE		= DwtMenuItem.SELECT_STYLE;
132 
133 DwtMenuItem._MENU_POPUP_DELAY	= 250;
134 DwtMenuItem._MENU_POPDOWN_DELAY	= 250;
135 
136 //
137 // Data
138 //
139 
140 DwtMenuItem.prototype.TEMPLATE					= "dwt.Widgets#ZMenuItem";
141 DwtMenuItem.prototype.SEPARATOR_TEMPLATE		= "dwt.Widgets#ZMenuItemSeparator";
142 DwtMenuItem.prototype.BLANK_CHECK_TEMPLATE		= "dwt.Widgets#ZMenuItemBlankCheck";
143 DwtMenuItem.prototype.BLANK_ICON_TEMPLATE		= "dwt.Widgets#ZMenuItemBlankIcon";
144 DwtMenuItem.prototype.BLANK_CASCADE_TEMPLATE	= "dwt.Widgets#ZMenuItemBlankCascade";
145 
146 //
147 // Public methods
148 //
149 DwtMenuItem.prototype.dispose =
150 function() {
151 	delete this._checkEl;
152 	DwtButton.prototype.dispose.call(this);
153 };
154 
155 /**
156  * Creates the menu item.
157  * 
158  * @param	{hash}	params		a hash of parameters
159  */
160 DwtMenuItem.create =
161 function(params) {
162 	var mi = new DwtMenuItem(params);
163 	if (params.imageInfo) {
164 		mi.setImage(params.imageInfo);
165 	}
166 	if (params.text) {
167 		mi.setText(params.text);
168 	}
169 	mi.setEnabled(params.enabled !== false);
170 	return mi;
171 };
172 
173 /**
174  * Gets the checked flag.
175  * 
176  * @return	{boolean}	<code>true</code> if the item is checked
177  */
178 DwtMenuItem.prototype.getChecked =
179 function() {
180 	return this._itemChecked;
181 };
182 
183 /**
184  * Sets the checked flag.
185  * 
186  * @param	{boolean}	checked			if <code>true</code>, check the item
187  * @param	{boolean}	skipNotify		if <code>true</code>, do not notify listeners
188  */
189 DwtMenuItem.prototype.setChecked =
190 function(checked, skipNotify) {
191 	this._setChecked(checked, null, skipNotify);
192 	this.parent._checkItemAdded(this);
193 };
194 
195 DwtMenuItem.prototype.setImage =
196 function(imageInfo) {
197 	DwtButton.prototype.setImage.call(this, imageInfo);
198 	this.parent._iconItemAdded(this);
199 };
200 
201 DwtMenuItem.prototype.setText =
202 function(text) {
203 	DwtButton.prototype.setText.call(this, text);
204 	if (this.parent.isPoppedUp()) {
205 		// resize menu if we reset text on the fly
206 		this.parent.render();
207 	}
208 };
209 
210 DwtMenuItem.prototype.setMenu =
211 function(params) {
212 	var params = Dwt.getParams(arguments, DwtButton.setMenuParams);
213 	DwtButton.prototype.setMenu.call(this, params);
214 	this.parent._submenuItemAdded(this);
215 };
216 
217 DwtMenuItem.prototype.setHoverDelay =
218 function(delay) {
219 	this._hoverDelay = delay;
220 };
221 
222 DwtMenuItem.prototype.setShortcut =
223 function(shortcut) {
224 	if (shortcut && this._dropDownEl) {
225 		this._dropDownEl.innerHTML = shortcut;
226 	}
227 };
228 
229 // Set whether the item is selectable even when it has an open submenu
230 DwtMenuItem.prototype.setSelectableWithSubmenu =
231 function(selectable) {
232 	this._selectableWithSubmenu = selectable;
233 };
234 
235 DwtMenuItem.prototype.isSeparator =
236 function() {
237 	return Boolean(this._style & DwtMenuItem.SEPARATOR_STYLE);
238 };
239 
240 DwtMenuItem.prototype.handleKeyAction =
241 function(actionCode, ev) {
242 	if (this.parent) {
243 		return this.parent.handleKeyAction(actionCode, ev)
244 	} else {
245 		return DwtButton.prototype.handleKeyAction.call(this, actionCode, ev);
246 	}
247 };
248 
249 DwtMenuItem.prototype.getKeyMapName =
250 function() {
251 	return DwtKeyMap.MAP_MENU;
252 };
253 
254 //
255 // Protected methods
256 //
257 
258 DwtMenuItem.prototype._createHtml =
259 function(templateId) {
260 	var defaultTemplate = this.isSeparator() ? this.SEPARATOR_TEMPLATE : this.TEMPLATE;
261 	DwtButton.prototype._createHtml.call(this, templateId || defaultTemplate);
262 };
263 
264 DwtMenuItem.prototype._createHtmlFromTemplate =
265 function(templateId, data) {
266 	DwtButton.prototype._createHtmlFromTemplate.call(this, templateId, data);
267 	this._checkEl = document.getElementById(data.id+"_check");
268 };
269 
270 DwtMenuItem.prototype._setChecked =
271 function(checked, ev, skipNotify) {
272 	var isCheck = this._style & DwtMenuItem.CHECK_STYLE;
273 	var isRadio = this._style & DwtMenuItem.RADIO_STYLE;
274 	if ((isCheck || isRadio) && this._itemChecked != checked) {
275 		this._itemChecked = checked;
276 
277 		if (this._checkEl) {
278 			this._checkEl.innerHTML = "";
279 			var icon = checked ? (isCheck ? "MenuCheck" : "MenuRadio") : "Blank_9";
280 			AjxImg.setImage(this._checkEl, icon);
281 			if (checked) {
282 				// deselect currently selected radio button
283 				if (isRadio) {
284 					this.parent._radioItemSelected(this, skipNotify);
285 				}
286 			}
287 		}
288 	}
289 };
290 
291 DwtMenuItem.prototype._checkItemAdded = function(className) {};
292 DwtMenuItem.prototype._checkedItemsRemoved = function() {};
293 
294 DwtMenuItem.prototype._submenuItemAdded =
295 function() {
296 	if (this.isSeparator()) { return; }
297 
298 	if (this._cascCell == null) {
299 		this._cascCell = this._row.insertCell(-1);
300 		this._cascCell.noWrap = true;
301 	}
302 };
303 
304 DwtMenuItem.prototype._submenuItemRemoved =
305 function() {
306 	if (this._dropDownEl) {
307 		this._dropDownEl.innerHTML = "";
308 	}
309 };
310 
311 DwtMenuItem.prototype._popupMenu =
312 function(delay, kbGenerated) {
313 	var menu = this.getMenu();
314 	var pp = this.parent.parent;
315 	var pb = this.getBounds();
316 	var ws = menu.shell.getSize();
317 	var s = menu.getSize();
318 	var x;
319 	var y;
320 	var vBorder;
321 	var hBorder;
322 	var ppHtmlElement = pp.getHtmlElement();
323 	if (pp._style == DwtMenu.BAR_STYLE) {
324 		vBorder = (ppHtmlElement.style.borderLeftWidth == "") ? 0 : parseInt(ppHtmlElement.style.borderLeftWidth);
325 		x = pb.x + vBorder;
326 		hBorder = (ppHtmlElement.style.borderTopWidth == "") ? 0 : parseInt(ppHtmlElement.style.borderTopWidth);
327 		hBorder += (ppHtmlElement.style.borderBottomWidth == "") ? 0 : parseInt(ppHtmlElement.style.borderBottonWidth);
328 		y = pb.y + pb.height + hBorder;		
329 		x = ((x + s.x) >= ws.x) ? x - (x + s.x - ws.x): x;
330 	}
331 	else { // Drop Down
332 		vBorder = (ppHtmlElement.style.borderLeftWidth == "") ? 0 : parseInt(ppHtmlElement.style.borderLeftWidth);
333 		vBorder += (ppHtmlElement.style.borderRightWidth == "") ? 0 : parseInt(ppHtmlElement.style.borderRightWidth);
334 		x = pb.x + pb.width + vBorder;
335 		hBorder = (ppHtmlElement.style.borderTopWidth == "") ? 0 : parseInt(ppHtmlElement.style.borderTopWidth);
336 		y = pb.y + hBorder;
337         if (menu.centerOnParentVertically()) {
338             y += pb.height / 2;
339         }
340 		//x = ((x + s.x) >= ws.x) ? pb.x - s.x - vBorder : x;
341 	}
342 	menu.popup(delay, x, y, kbGenerated);
343 };
344 
345 DwtMenuItem.prototype._popdownMenu =
346 function() {
347 	var menu = this.getMenu();
348 	if (menu) {
349 		menu.popdown();
350 	}
351 };
352 
353 DwtMenuItem.prototype._isMenuPoppedUp =
354 function() {
355 	var menu = this.getMenu();
356 	return (menu && menu.isPoppedUp());
357 };
358 
359 DwtMenuItem.prototype._blur =
360 function() {
361 	var menu = this.parent;
362 	var mev = new DwtMouseEvent();
363 
364 	this._setMouseEvent(mev, {
365 		dwtObj: this,
366 		ersatz: true,
367 		type: DwtEvent.ONMOUSEOUT
368 	});
369 	this.notifyListeners(DwtEvent.ONMOUSEOUT, mev);
370 	menu.__currentItem = null;
371 
372 	DwtButton.prototype._blur.call(this);
373 };
374 
375 DwtMenuItem.prototype._focus = function() {
376 
377 	var menu = this.parent;
378 	var currItem = menu.__currentItem;
379 	var mev = new DwtMouseEvent();
380 
381 	this._setMouseEvent(mev, {dwtObj:this, ersatz: true});
382 	this.notifyListeners(AjxEnv.isIE ? DwtEvent.ONMOUSEENTER : DwtEvent.ONMOUSEOVER, mev);	// mouseover selects a menu item
383 	menu.__currentItem = this;
384 
385 	DwtButton.prototype._focus.call(this);
386 }
387 
388 //
389 // Private methods
390 //
391 
392 DwtMenuItem.prototype.__handleItemSelect =
393 function(event) {
394 	this.setDisplayState(DwtControl.NORMAL);
395 	if (this.isStyle(DwtMenuItem.CHECK_STYLE)) {
396 		this._setChecked(!this._itemChecked, null, true);
397 		event.detail = this.getChecked() ? DwtMenuItem.CHECKED : DwtMenuItem.UNCHECKED;
398 	}
399 	else if (this.isStyle(DwtMenuItem.RADIO_STYLE)) {
400 		this._setChecked(true, true);
401 		this.parent._radioItemSelected(this, true);
402 		event.detail = this.getChecked() ? DwtMenuItem.CHECKED : DwtMenuItem.UNCHECKED;
403 	}
404 	else if (this.isStyle(DwtMenuItem.PUSH_STYLE)) {
405 		if (this._menu) {
406 			if (this._isMenuPoppedUp()) {
407 				DwtMenu.closeActiveMenu(event);
408 			}
409 			else {
410 				this._popupMenu(DwtKeyEvent.isKeyEvent(ev));
411 			}
412 		}
413 		return;
414 	}
415 	if (!this.isStyle(DwtMenuItem.CASCADE_STYLE)) {
416 		if (this._selectableWithSubmenu || !this._menu || !this._menu.isPoppedUp || !this._menu.isPoppedUp()) {
417 			DwtMenu.closeActiveMenu(event);
418 		}
419 	}
420 };
421 
422 DwtMenuItem._mouseOverListener =
423 function(ev) {
424 	var menuItem = ev.dwtObj;
425 	if (!menuItem) { return false; }
426 	var menu = menuItem.parent;
427 	if (menuItem.isSeparator()) { return false; }
428 	DwtButton._mouseOverListener(ev, menuItem);
429 	if (!ev.ersatz) {
430 		menu._popdownSubmenus();
431 	}
432 	if (!menuItem.hasFocus() && menuItem.getEnabled()) {
433 		menuItem.focus();
434 	}
435 
436 	if (menuItem._menu && !ev.ersatz) {
437 		menuItem._popupMenu(menuItem._hoverDelay);
438 	}
439 };
440 
441 DwtMenuItem._mouseOutListener =
442 function(ev) {
443 	var menuItem = ev.dwtObj;
444 	var submenu = menuItem && menuItem.getMenu();
445 	if (submenu && submenu.isPoppedUp()) { return; }
446 	DwtButton._mouseOutListener(ev);
447 };
448 
449 /*
450  * returns menu item table row element
451  */
452 DwtMenuItem.prototype.getRowElement =
453 function() {
454 	var el = this._textEl ||
455 		this._dropDownEl ||
456 		(this._iconEl && this._iconEl.left) ||
457 		(this._iconEl && this._iconEl.right);
458 	if (el) {
459 		return el.parentNode;
460 	}
461 };
462 
463 DwtMenuItem._listeners = {};
464 DwtMenuItem._listeners[DwtEvent.ONMOUSEOVER]	= DwtMenuItem._mouseOverListener.bind();
465 DwtMenuItem._listeners[DwtEvent.ONMOUSEOUT]		= DwtMenuItem._mouseOutListener.bind();
466 DwtMenuItem._listeners[DwtEvent.ONMOUSEDOWN]	= DwtButton._mouseDownListener.bind();
467 DwtMenuItem._listeners[DwtEvent.ONMOUSEUP]		= DwtButton._mouseUpListener.bind();
468 DwtMenuItem._listeners[DwtEvent.ONMOUSEENTER]	= DwtMenuItem._mouseOverListener.bind();
469 DwtMenuItem._listeners[DwtEvent.ONMOUSELEAVE]	= DwtButton._mouseOutListener.bind();
470