1 include(bbq.lang.Delegator);
  2 include(bbq.web.DOMUtil);
  3 
  4 bbq.gui.GUIWidget = new Class.create(bbq.lang.Delegator, /** @lends bbq.gui.GUIWidget.prototype */ {
  5 	/**
  6 	 * This property holds the point at which this widget is attached to the DOM.
  7 	 *
  8 	 * @type {Node}
  9 	 */
 10 	_rootNode: null,
 11 
 12 	/**
 13 	 * <p>This class is used as a base for all objects that have GUI representations.</p>
 14 
 15 	 * <p>The constructor method of this class should always be called explicitly by child classes.</p>
 16 	 *
 17 	 * <p>The rootNode property is a DOM node that is attached to the DOM tree after the object is
 18 	 * created via the appendTo method.</p>
 19 	 *
 20 	 * <pre><code class="language-javascript">
 21 	 * com.myapp.MyWidget = new Class.create(bbq.gui.GUIWidget, {
 22 	 *      _greeting: null,
 23 	 *
 24 	 *      initialize: function($super, options) {
 25 	 *          $super(options);
 26 	 *
 27 	 *          this._greeting = DOMUtil.createElement("p", "Hello world!");
 28 	 *      },
 29 	 *
 30 	 *      render: function() {
 31 	 *          this.empty();
 32 	 *
 33 	 *          this.appendChild(this._greeting);
 34 	 *      }
 35 	 * });
 36 	 *
 37 	 * ...
 38 	 *
 39 	 * var widget = new com.myapp.MyWidget({
 40 	 *     attributes: {
 41 	 *         className: "Foo",
 42 	 *         style: {
 43 	 *             backgroundColour: "red"
 44 	 *         }
 45 	 *     }
 46 	 * });
 47 	 * widget.appendTo(document.body);
 48 	 * </code></pre>
 49 	 *
 50 	 * @memberOf bbq.gui
 51 	 * @constructs
 52 	 * @param {Object} options
 53 	 * @param {Object} [options.attributes] A key/value object of attributes to be applied to the root node
 54 	 * @extends bbq.lang.Delegator
 55 	 */
 56 	initialize: function($super, options) {
 57 		$super(options);
 58 		
 59 		this.setRootNode("div");
 60 	},
 61 	
 62 	/**
 63 	 * <p>The idea of the method is to create a DOM node tree representation of the widget.  The root node of the tree should be
 64 	 * this._rootNode.  The presence of any other nodes in the DOM tree which are not descendants of this._rootNode should not
 65 	 * be relied upon.</p>
 66 	 * 
 67 	 * <p>This method will be called before the node tree is added to the main document tree (in a similar way to off-screen buffering
 68 	 * in graphics programming) and may be called at seeming arbitrary times.  Consequently it should always create a 
 69 	 * representation of the widget/object in it's current state but at the same time, not rely on any other portion of the DOM tree 
 70 	 * existing.</p>
 71 	 */
 72 	render: function() {
 73 		
 74 	},
 75 	
 76 	/**
 77 	 * <p>Sets the root node and registers this object for retrieval from the root node.</p>
 78 	 * 
 79 	 * <p>This object can then be retrieved by calling owner() on the dom node passed as the argument to this method.</p>
 80 	 * 
 81 	 * @param {Node or String} rootNode Either a DOM Node (from document.createElement) or the String name of a node - e.g. "div"
 82 	 * @example
 83 	 * <pre><code class="language-javascript">
 84 	 * bbq.gui.form.TextField = new Class.create(bbq.gui.GUIWidget, {
 85 	 *     initialize: function($super, options) {
 86 	 *       $super(options);
 87 	 *
 88 	 *       this.setRootNode(document.createElement("input"));
 89 	 *       this.addClass("TextField");
 90 	 *       this.setAttribute("value", this.options.value);
 91 	 *   }
 92 	 * });
 93 	 * 
 94 	 * ...
 95 	 * 
 96 	 * var foo = new bbq.gui.form.TextField({value: "a value"});
 97 	 * foo.appendTo(document.body);
 98 	 * 
 99 	 * ...
100 	 * 
101 	 * var bar = document.getElementById("exampleObject");
102 	 * var foo = bar.owner();
103 	 * </code></pre>
104 	 */
105 	setRootNode: function(rootNode) {
106 		if(this._rootNode) {
107 			var oldNode = this._rootNode;
108 		}
109 
110 		if(Object.isString(rootNode)) {
111 			this._rootNode = document.createElement(rootNode);
112 		} else {
113 			this._rootNode = rootNode;
114 		}
115 
116 		if(oldNode && oldNode.className) {
117 			this.addClass(oldNode.className.split(" "));
118 		}
119 
120 		if(this.options && this.options.attributes) {
121 			var attr = this.options.attributes;
122 			for(var key in attr) {
123 				this.setAttribute(key, attr[key]);
124 			}
125 		}
126 
127 		this._rootNode = $(this._rootNode);
128 		this.registerObject();
129 	},
130 	
131 	getRootNode: function() {
132 		return this._rootNode;
133 	},
134 
135 	/**
136 	 * Sets a reference to this object on the rootNode DOM node.  This allows us to take a DOM node from the document tree and 
137 	 * get the GUIWidget object of which it is the root node.
138 	 * 
139 	 * @see bbq.gui.GUIWidget#setRootNode
140 	 */
141 	registerObject: function() {
142 		if(this._rootNode) {
143 			this._rootNode.owner = function() {
144 				return this;
145 			}.bind(this);
146 		}
147 	},
148 	
149 	/**
150 	 * Removes all nodes attached to this GUIWidgets rootNode
151 	 */
152 	empty: function() {
153 		if(this._rootNode) {
154 			DOMUtil.emptyNode(this._rootNode);
155 		}
156 	},
157 
158 	hasClass: function(className) {
159 		return DOMUtil.hasClass(this.getRootNode(), className);
160 	},
161 	
162 	/**
163 	 * Adds a class to the root node
164 	 *
165 	  * @param	{String}	className	The name of the class to add
166 	 */
167 	addClass: function(className) {
168 		DOMUtil.addClass(this.getRootNode(), className);
169 	},
170 	
171 	/**
172 	 * Removes a class from the root node
173 	 * 
174 	 * @param	{String}	className	The name of the class to remove
175 	 */
176 	removeClass: function(className, recursively) {
177 		DOMUtil.removeClass(this.getRootNode(), className, recursively);
178 	},
179 	
180 	/**
181 	 * Attaches the root node to the passed node
182 	 * 
183 	 * @param	{Node}	pageNode	The node to attach the root node to
184 	 */
185 	appendTo: function(pageNode) {
186 		try {
187 			if(pageNode) {
188 				pageNode = this._getNode(pageNode);
189 				pageNode.appendChild(this.getRootNode());
190 			}
191 			
192 			this.render();
193 		} catch(e) {
194 			Log.error("Error while appending to node " + pageNode, e);
195 		}
196 	},
197 
198 	/**
199 	 * Adds this GUIWidget to the DOM in front of the passed node
200 	 *
201 	 * @param Node node
202 	 */
203 	appendBefore: function(node) {
204 		try {
205 			node = this._getNode(node);
206 			var parentNode = node.parentNode;
207 			parentNode.insertBefore(this.getRootNode(), node);
208 
209 			this.render();
210 		} catch(e) {
211 			Log.error("Error while appending before " + node, e);
212 		}
213 	},
214 
215 	appendAfter: function(node) {
216 		try {
217 			node = this._getNode(node);
218 
219 			// find the parent node
220 			var parentNode = node.parentNode;
221 
222 			// find the index of the node
223 			var nodeArray = $A(parentNode.childNodes);
224 			var index = nodeArray.indexOf(node);
225 			var nextNode = nodeArray[index + 1];
226 
227 			parentNode.insertBefore(this.getRootNode(), nextNode);
228 
229 			this.render();
230 		} catch(e) {
231 			Log.error("Error while appending after " + node, e);
232 		}
233 	},
234 	
235 	/**
236 	 * Adds a node or GUIWidget to the root node of this element
237 	 * 
238 	 * @param	{Mixed}	childNode	A Node, GUIWidget or array of Node and/or GUIWidget objects
239 	 */
240 	appendChild: function(childNode) {
241 		if(!childNode) {
242 			return;
243 		}
244 		
245 		if(childNode instanceof Array) {
246 			childNode.each(function(node) {
247 				this.appendChild(node);
248 			}.bind(this));
249 		} else {
250 			if(childNode && this._rootNode) {
251 				if(childNode.appendTo instanceof Function) {
252 					childNode.appendTo(this._rootNode);
253 				} else if(childNode.toUpperCase instanceof Function) {
254 					this._rootNode.appendChild(document.createTextNode(childNode));
255 				} else {
256 					this._rootNode.appendChild(childNode);
257 				}
258 			}
259 		}
260 		
261 		return childNode;
262 	},
263 	
264 	/**
265 	 * <p>Attempts to remove the passed node if it is a child of this._rootNode</p>
266 	 * 
267 	 * <p>The passed argument can be either a DOM Node object, or a GUIWidget object.</p>
268 	 * 
269 	 * @param	{Mixed}	A child node
270 	 */
271 	removeChild: function(childNode) {
272 		if(childNode && this.getRootNode()) {
273 			try {
274 				if(childNode.getRootNode instanceof Function) {
275 					this.getRootNode().removeChild(childNode.getRootNode());
276 				} else {
277 					this.getRootNode().removeChild(childNode);
278 				}
279 			} catch(e) {
280 				Log.error("Error removing child " + childNode, e);
281 			}
282 		}
283 	},
284 	
285 	/**
286 	 * Replaces a DOM node or GUIWidget that is a child of the root node of this object
287 	 * 
288 	 * @param	{Node || GUIWidget}		oldNode		The outgoing child
289 	 * @param	{Node || GUIWidget}		newNode		The incoming child
290 	 * @return {Node || GUIWidget}		The incoming child
291 	 */
292 	replaceChild: function(oldNode, newNode) {
293 		var output = newNode;
294 
295 		if(oldNode && newNode && this.getRootNode()) {
296 			try {
297 				if(oldNode.getRootNode instanceof Function) {
298 					oldNode = oldNode.getRootNode();
299 				}
300 
301 				if(newNode.getRootNode instanceof Function) {
302 					newNode.render();
303 					newNode = newNode.getRootNode();
304 				}
305 
306 				// check that we actually contain the old node before attempting to replace it
307 				if(Element.descendantOf(oldNode, this.getRootNode())) {
308 					this.getRootNode().replaceChild(newNode, oldNode);
309 				}
310 			} catch(e) {
311 				Log.error("Error replacing child " + oldNode + " for " + newNode, e);
312 			}
313 		}
314 
315 		return output;
316 	},
317 	
318 	/**
319 	 * Sets the id attribute on the root node
320 	 * 
321 	 * @param	{string}	id
322 	 */
323 	setID: function(id) {
324 		this.setAttribute("id", id);
325 	},
326 	
327 	/**
328 	 * Returns the id of the root node
329 	 * 
330 	 * @return	{string}
331 	 */
332 	getID: function() {
333 		this.getAttribute("id");
334 	},
335 	
336 	/**
337 	 * Sets a CSS style property to the passed value on the root node
338 	 * 
339 	 * @param	{string}	styleName	e.g. "border"
340 	 * @param	{string}	styleValue	e.g. "1px solid #CCC"
341 	 */
342 	setStyle: function(styleName, styleValue) {
343 		DOMUtil.setStyle(this.getRootNode(), styleName, styleValue);
344 	},
345 	
346 	/**
347 	 * Returns the requested CSS style property
348 	 * 
349 	 * @param	{string}	styleName	e.g. "font"
350 	 * @return	{string}
351 	 */
352 	getStyle: function(styleName) {
353 		return DOMUtil.getStyle(this.getRootNode(), styleName);
354 	},
355 	
356 	/**
357 	 * Sets an attribute on the root node
358 	 * 
359 	 * @param	{String}	attributeName	e.g. "id"
360 	 * @param	{String}	attributeValue	e.g. "anElement"
361 	 */
362 	setAttribute: function(attributeName, attributeValue) {
363 		if(this._rootNode) {
364 			try {
365 				if(attributeName == "style" && attributeValue instanceof Object) {
366 					for(var key in attributeValue) {
367 						this.setStyle(key, attributeValue[key]);
368 					}
369 				} else if(this._rootNode[attributeName] != attributeValue) {
370 					this._rootNode[attributeName] = attributeValue;
371 				}
372 			} catch(e) {
373 				Log.error("exeption thrown setting " + attributeName + " to " + attributeValue, e);
374 			}
375 		}
376 	},
377 	
378 	/**
379 	 * Returns the requested attribute from the root node
380 	 * 
381 	 * @param	{String}	attributeName	e.g. "id"
382 	 * @return	{String}
383 	 */
384 	getAttribute: function(attributeName) {
385 		if(this._rootNode) {
386 			return this._rootNode[attributeName];
387 		}
388 	},
389 	
390 	/**
391 	 * Returns whether or not the root node of this object is of the passed CSS class
392 	 * 
393 	 * @return boolean
394 	 */
395 	isClass: function(className) {
396 		return DOMUtil.hasClass(this.getRootNode(), className);
397 	},
398 	
399 	/**
400 	 * @return void
401 	 */
402 	resize: function() {
403 		this.notifyListeners("onResize");
404 	},
405 	
406 	_sizeScrollable: function(scrollable, dim, maxSize) {
407 		if(scrollable && scrollable.parentNode) {
408 			scrollable.owner().triggerEvent("onWillResize");
409 			var nodeList = $A(scrollable.parentNode.childNodes);
410 			
411 			nodeList.each(function(node){
412 				if(node && node != scrollable) {
413 						var dims = Element.getDimensions(node);
414 						maxSize = maxSize - dims[dim];
415 				}
416 			});
417 			
418 			scrollable.style[dim] = maxSize + "px";
419 			scrollable.owner().triggerEvent("onResize");
420 		}
421 	},
422 	
423 	/**
424 	 * Makes the passed node the first child of this object.
425 	 * 
426 	 * @param {Object} A GUIWidget or DOM Node
427 	 */
428 	insertAtTop: function(newNode) {
429 		this.getRootNode().insertBefore(this._getNode(newNode), this.getRootNode().firstChild);
430 	},
431 	
432 	/**
433 	 * Inserts the first passed node before the second.
434 	 * 
435 	 * @param {Object} A GUIWidget or DOM Node
436 	 * @param {Object} A GUIWidget or DOM Node - should be a child of this element
437 	 */
438 	insertBefore: function(newNode, referenceNode) {
439 		this.getRootNode().insertBefore(this._getNode(newNode), this._getNode(referenceNode));
440 	},
441 	
442 	/**
443 	 * Gets the DOM node from a GUIWidget
444 	 * 
445 	 * @param {Object} A GUIWidget or DOM Node
446 	 * @return {Node}
447 	 */
448 	_getNode: function(fromNode) {
449 		if(fromNode) {
450 			if(fromNode.getRootNode instanceof Function) {
451 				return fromNode.getRootNode();
452 			} else if(fromNode.appendChild) {
453 				return fromNode;
454 			}
455 		}
456 
457 		Log.error("Invalid node!");
458 	},
459 
460 	/**
461 	 * Causes this GUIWidget to gain focus
462 	 *
463 	 * @see bbq.gui.GUIWidget#blur
464 	 */
465 	focus: function() {
466 		this.getRootNode().focus();
467 	},
468 
469 	/**
470 	 * Causes this GUIWidget to lose focus
471 	 *
472 	 * @see bbq.gui.GUIWidget#focus
473 	 */
474 	blur: function() {
475 		this.getRootNode().blur();
476 	},
477 
478 	/**
479 	 * Shows this GUIWidget if hidden
480 	 *
481 	 * @see bbq.gui.GUIWidget#hide
482 	 */
483 	show: function() {
484 		this.getRootNode().show();
485 	},
486 
487 	/**
488 	 * Hides this GUIWidget
489 	 *
490 	 * @see bbq.gui.GUIWidget#show
491 	 */
492 	hide: function() {
493 		this.getRootNode().hide();
494 	}
495 });
496