1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 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) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file contains a Dwt composite control.
 27  * 
 28  */
 29 
 30 /**
 31  * @class
 32  * A composite may contain other controls. All controls that need to contain child controls
 33  * (such as menus, trees) should inherit from this class.
 34  * 
 35  * @param {hash}	params		a hash of parameters
 36  * @param {DwtComposite}	params.parent	the parent widget
 37  * @param {string}	params.className		the CSS class
 38  * @param {constant}	params.posStyle		the positioning style
 39  * @param {boolean}	params.deferred		if <code>true</code>, postpone initialization until needed
 40  * @param {string}	params.id			an explicit ID to use for the control's HTML element
 41  * @param {number}	params.index 		the index at which to add this control among parent's children
 42  * 
 43  * @extends	DwtControl
 44  */
 45 DwtComposite = function(params) {
 46 	if (arguments.length == 0) { return; }
 47 	params = Dwt.getParams(arguments, DwtComposite.PARAMS);
 48 	
 49 	params.className = params.className || "DwtComposite";
 50 	DwtControl.call(this, params);
 51 
 52 	var desc = this.toString();
 53 	if (desc == 'DwtComposite') {
 54 		desc = this.getHTMLElId();
 55 	}
 56 
 57 	this._compositeTabGroup = new DwtTabGroup(desc + ' (DwtComposite)');
 58 
 59 	/**
 60 	 * Vector of child elements
 61 	 * @type AjxVector
 62 	 */
 63 	this._children = new AjxVector();
 64 }
 65 
 66 DwtComposite.PARAMS = DwtControl.PARAMS.concat();
 67 
 68 DwtComposite.prototype = new DwtControl;
 69 DwtComposite.prototype.constructor = DwtComposite;
 70 
 71 DwtComposite.prototype.isDwtComposite = true;
 72 DwtComposite.prototype.toString = function() { return "DwtComposite"; }
 73 
 74 
 75 
 76 /**
 77  * Pending elements hash (i.e. elements that have not yet been realized).
 78  * @private
 79  */
 80 DwtComposite._pendingElements = new Object();
 81 
 82 
 83 /**
 84  * Disposes of the control. This method will remove the control from under the
 85  * control of it's parent and release any resources associate with the component.
 86  * The method will also notify any event listeners on registered {@link DwtEvent.DISPOSE} event type.
 87  * 
 88  * <p>
 89  * In the case of {@link DwtComposite} this method will also dispose of all of the composite's
 90  * children.
 91  * 
 92  * <p> 
 93  * Subclasses may override this method to perform their own dispose functionality but
 94  * should generally call the parent <code>dispose()</code> method.
 95  * 
 96  * @see DwtControl#isDisposed
 97  * @see DwtControl#addDisposeListener
 98  * @see DwtControl#removeDisposeListener
 99  */
100 DwtComposite.prototype.dispose =
101 function() {
102 	if (this._disposed) return;
103 
104 	var children = this._children.getArray();
105 	while (children.length > 0) {
106         children.pop().dispose();
107 	}
108 
109 	if (this._compositeTabGroup) {
110 		this._compositeTabGroup.removeAllMembers();
111 	}
112 	this._compositeTabGroup = null;
113 
114 	DwtControl.prototype.dispose.call(this);
115 }
116 
117 /**
118  * Get a list of children of this composite.
119  * 
120  * @return	{array}		an array of {@link DwtControl} objects
121  */
122 DwtComposite.prototype.getChildren =
123 function() {
124 	return this._children.getArray().slice(0);
125 }
126 
127 /**
128  * Get the Nth child of this composite.
129  * 
130  * @param {number}	index 		the index of the child.
131  *
132  * @return	{DwtControl}		the child.
133  */
134 DwtComposite.prototype.getChild =
135 function(idx) {
136 	return this._children.get(idx);
137 };
138 
139 /**
140  * collapses consecutive separators into one. Gets rid of head or tail separators as well .
141  * Note that is does not remove the separators, just hides them so they can re-displayed as needed, next time this is called and other elements
142  * become visible
143  *
144  * this would be used on such subclasses as DwtMenu and DwtToolbar .
145  * However, currently it does not work with the toolbars, since separators there are not added as children to the toolbar composite.
146  * I tried to make it consistent with the DwtMenu approach, but it seemed a bit complicated right now.
147  * so for now I try to make it so no complete groups (items between separators) are hidden at one time. It might also be possible
148  * to do it for the toolbar using the _items HTML elements array, but probably less elegant than this approach.
149  */
150 DwtComposite.prototype.cleanupSeparators =
151 function() {
152 	var items = this.getChildren();
153 	var previousVisibleIsSeparator = true; // I lie so that upfront separator would be cleaned up
154 	var lastSeparator;
155 	for (var i = 0; i < items.length; i++) {
156 		var item = items[i];
157 		var isSeparator = item.isStyle && item.isStyle(DwtMenuItem.SEPARATOR_STYLE);
158 
159 		if (isSeparator) {
160 			item.setVisible(!previousVisibleIsSeparator);
161 			if (!previousVisibleIsSeparator || !lastSeparator) { //the !lastSeparator is the edge case of first item is separator. (see comment about lie above)
162 				//keep track of last visible separator (if it's also last item visible overall)
163 				previousVisibleIsSeparator = true;
164 				lastSeparator = item;
165 			}
166 			continue;
167 		}
168 
169 		//not a separator
170 		if (item.getVisible()) {
171 			previousVisibleIsSeparator = false;
172 		}
173 	}
174 	//cleanup tail separator
175 	if (previousVisibleIsSeparator && lastSeparator) {
176 		lastSeparator.setVisible(false);
177 	}
178 };
179 
180 
181 
182 
183 /**
184  * Gets the number of children of this composite.
185  * 
186  * @return {number} 		the number of composite children
187  */
188 DwtComposite.prototype.getNumChildren =
189 function() {
190 	return this._children.size();
191 }
192 
193 /**
194  * Removes all of the composite children.
195  * 
196  */
197 DwtComposite.prototype.removeChildren =
198 function() {
199 	var a = this._children.getArray();
200 	while (a.length > 0) {
201 		a[0].dispose();
202 	}
203 	if (this._compositeTabGroup) {
204 		this._compositeTabGroup.removeAllMembers();
205 	}
206 }
207 
208 /**
209  * Clears the composite HTML element of content and removes
210  * all composite children by calling <code>removeChildren</code>.
211  * 
212  * @see #removeChildren
213  */
214 DwtComposite.prototype.clear =
215 function() {
216 	this.removeChildren();
217 	this.getHtmlElement().innerHTML = "";
218 }
219 
220 /**
221 * Adds the given child control to this composite at the index (if specified).
222 *
223 * @param {DwtControl} child		the child control to add
224 * @param {number}	index		the index at which to add the child (may be <code>null</code>)
225 */
226 DwtComposite.prototype.addChild =
227 function(child, index) {
228 	this._children.add(child, index);
229 	this._compositeTabGroup.addMember(child, index);
230 	
231 	// check for a previously removed element
232 	var childHtmlEl = child.getHtmlElement();
233 	childHtmlEl.setAttribute("parentId", this._htmlElId);
234 	if (this instanceof DwtShell && this.isVirtual()) {
235 		// If we are operating in "virtual shell" mode, then children of the shell's html elements
236 		// are actually parented to the body
237 		document.body.appendChild(childHtmlEl);
238 	} else {
239 		child.reparentHtmlElement(child.__parentElement || this.getHtmlElement(), index);
240 		child.__parentElement = null; // don't keep the reference to element, if any
241 	}
242 };
243 
244 /**
245 * Removes the specified child control from this control. A removed child is no longer retrievable via
246 * <code>getHtmlElement()</code>, so there is an option to save a reference to the removed child. 
247 * That way it can be added later using <code>addChild()</code>.
248 *
249 * @param {DwtConrol} child		the child control to remove
250 * @see #addChild
251 */
252 DwtComposite.prototype.removeChild =
253 function(child) {
254 	DBG.println(AjxDebug.DBG3, "DwtComposite.prototype.removeChild: " + child._htmlElId + " - " + child.toString());
255 	// Make sure that the child is initialized. Certain children (such as DwtTreeItems)
256 	// can be created in a deferred manner (i.e. they will only be initialized if they
257 	// are becoming visible.
258 	if (child.isInitialized()) {
259 		this._children.remove(child);
260 		this._compositeTabGroup.removeMember(child);
261 		// Sometimes children are nested in arbitrary HTML so we elect to remove them
262 		// in this fashion rather than use this.getHtmlElement().removeChild(child.getHtmlElement()
263 		var childHtmlEl = child.getHtmlElement();
264         if (childHtmlEl) {
265 			childHtmlEl.removeAttribute("parentId");
266 			if (childHtmlEl.parentNode) {
267 				var el = childHtmlEl.parentNode.removeChild(childHtmlEl);
268 			}
269 		}
270 	}
271 }
272 
273 /**
274  * Return this.tabGroupMember if present (it always overrides any other contender), otherwise if this composite has
275  * children return the composite tab group, otherwise just return this control (instead of a group with one member).
276  *
277  * @returns {DwtComposite|DwtTabGroup}
278  */
279 DwtComposite.prototype.getTabGroupMember = function() {
280 
281 	return this.tabGroupMember || (this.getNumChildren() > 0 ? this._compositeTabGroup : this);
282 };
283 
284 /**
285  * Allows the user to use the mouse to select text on the control.
286  * 
287  * @private
288  */
289 DwtComposite.prototype._setAllowSelection =
290 function() {
291 	if (!this._allowSelection) {
292 		this._allowSelection = true;
293 		this.addListener(DwtEvent.ONMOUSEDOWN, new AjxListener(this, this._mouseDownListener));
294 		this.addListener(DwtEvent.ONCONTEXTMENU, new AjxListener(this, this._contextMenuListener));
295 	}
296 };
297 
298 /**
299  * Sets whether to prevent the browser from allowing text selection.
300  * 
301  * @see DwtControl#preventSelection
302  * @private
303  */
304 DwtComposite.prototype.preventSelection = 
305 function(targetEl) {
306 	return this._allowSelection ? false : DwtControl.prototype.preventSelection.call(this, targetEl);
307 };
308 
309 /**
310  * Determines whether to prevent the browser from displaying its context menu.
311  * 
312  * @see DwtControl#preventContextMenu
313  * @private
314  */
315 DwtComposite.prototype.preventContextMenu =
316 function(target) {
317 	if (!this._allowSelection) {
318 		return DwtControl.prototype.preventContextMenu.apply(this, arguments);
319 	}
320 	
321 	var bObjFound = target ? (target.id.indexOf("OBJ_") == 0) : false;
322 	var bSelection = false;
323 
324 	// determine if anything has been selected (IE and mozilla do it differently)
325 	if (document.selection) {			// IE
326 		bSelection = document.selection.type == "Text";
327 	} else if (getSelection()) {		// mozilla
328 		bSelection = getSelection().toString().length > 0;
329 	}
330 
331 	// if something has been selected and target is not a custom object,
332 	return (bSelection && !bObjFound) ? false : true;
333 };
334 
335 /**
336  * Handles focus control when the mouse button is released
337  * 
338  * @see DwtControl#_focusByMouseUpEvent
339  * @private
340  */
341 DwtComposite.prototype._focusByMouseUpEvent =
342 function()  {
343 	if (!this._allowSelection) {
344 		DwtControl.prototype._focusByMouseUpEvent.apply(this, arguments);
345 	}
346 	// ...Else do nothing....
347 	// When text is being selected, we don't want the superclass
348 	// to give focus to the keyboard input control.
349 };
350 
351 /**
352  * Event listener that is only registered when this control allows selection
353  * 
354  * @see _allowSelection
355  * @private
356  */
357 DwtComposite.prototype._mouseDownListener =
358 function(ev) {
359 	if (ev.button == DwtMouseEvent.LEFT) {
360 		// reset mouse event to propagate event to browser (allows text selection)
361 		//todo - look into changing this, it's currently very confusing and inconsistent.
362 		//bug 23462 change it to stop propagation, so supposedly it should NOT allow selection.
363 		// (so the above comment is wrong). But it's more complicated than this, since ev._dontCallPreventDefault is more
364 		// important, and is not set here, so a listener could set it to TRUE thus making it allow selection, despite
365 		// _stopPropagation being set to true here. (not sure what the meaning is).
366 		// Note for example the inconsistency with the DwtComposite.prototype._contextMenuListener method below
367 		// As one cool way to allow selection look at ZmConvView2Header constructor, the line:
368 		// this.setEventPropagation(true, [DwtEvent.ONMOUSEDOWN, DwtEvent.ONSELECTSTART, DwtEvent.ONMOUSEUP, DwtEvent.ONMOUSEMOVE]);
369 		// which causes _dontCallPreventDefault to be set to true, not being overriden here, thus selection works.
370 		ev._stopPropagation = true;
371 		ev._returnValue = true;
372 	}
373 };
374 
375 /**
376  * Event listener that is only registered when this control allows selection
377  * 
378  * @see _allowSelection
379  * @private
380  */
381 DwtComposite.prototype._contextMenuListener =
382 function(ev) {
383 	// reset mouse event to propagate event to browser (allows context menu)
384 	ev._stopPropagation = false;
385 	ev._returnValue = true;
386 };
387