1 /**
  2  * The Render Engine
  3  * BaseObject
  4  *
  5  * @fileoverview The object from which most renderable engine objects will
  6  *               need to derive.
  7  *
  8  * @author: Brett Fattori (brettf@renderengine.com)
  9  * @author: $Author: bfattori $
 10  * @version: $Revision: 1573 $
 11  *
 12  * Copyright (c) 2011 Brett Fattori (brettf@renderengine.com)
 13  *
 14  * Permission is hereby granted, free of charge, to any person obtaining a copy
 15  * of this software and associated documentation files (the "Software"), to deal
 16  * in the Software without restriction, including without limitation the rights
 17  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 18  * copies of the Software, and to permit persons to whom the Software is
 19  * furnished to do so, subject to the following conditions:
 20  *
 21  * The above copyright notice and this permission notice shall be included in
 22  * all copies or substantial portions of the Software.
 23  *
 24  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 25  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 26  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 27  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 28  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 29  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 30  * THE SOFTWARE.
 31  *
 32  */
 33 
 34 // The class this file defines and its required classes
 35 R.Engine.define({
 36     "class":"R.engine.BaseObject",
 37     "requires":[
 38         "R.engine.PooledObject",
 39         "R.engine.Events"
 40     ]
 41 });
 42 
 43 /**
 44  * @class The base object class which represents an object within
 45  * the engine.  Objects of this type can have an associated DOM element,
 46  * and will also inherit the {@link #update} method to perform processing
 47  * for every frame generated.
 48  * <p/>
 49  * This object also enhances the event handling provided by the {@link R.engine.Events event engine}.
 50  * It will remember events assigned to the object so that they can be automatically
 51  * cleaned up when the object is destroyed.
 52  * <p/>
 53  * If you are working with an object that represents an HTML element (node),
 54  * this object is ideal to extend from.  It has methods for assigning and
 55  * accessing that element.
 56  * <p/>
 57  * The {@link #update} method is called each time a frame is generated by the engine
 58  * to update the object within the scene graph. In this method, you'll be able to
 59  * update the internals of the object and perform general housekeeping.
 60  *
 61  * @param name {String} The name of the object
 62  * @extends R.engine.PooledObject
 63  * @constructor
 64  * @description Create a base object.
 65  */
 66 R.engine.BaseObject = function () {
 67     "use strict";
 68     return R.engine.PooledObject.extend(/** @scope R.engine.BaseObject.prototype */{
 69 
 70         element:null,
 71         jQObject:undefined,
 72         events:null,
 73         eventListeners:null,
 74 
 75         /** @private */
 76         constructor:function (name) {
 77             this.base(name);
 78             this.events = {};
 79             this.eventListeners = {};
 80             this.jQObject = undefined;
 81         },
 82 
 83         /**
 84          * Destroy the object, cleaning up any events that have been
 85          * attached to this object.  Calls the <tt>destroy</tt> event
 86          * before the object is destroyed.
 87          */
 88         destroy:function () {
 89             // Trigger the "destroy" event before the object is destroyed
 90             this.triggerEvent("destroyed");
 91 
 92             // We need to make sure to remove any event's attached to us
 93             // that weren't already cleaned up
 94             for (var ref in this.events) {
 95                 var fn = this.events[ref];
 96                 var type = ref.split(",")[1];
 97                 if (fn) {
 98                     R.engine.Events.clearHandler(this.getElement(), type, fn);
 99                 }
100             }
101 
102             // Clean up non-element event listeners
103             this.eventListeners = {};
104 
105             if (this.element != null && this.element != document) {
106                 $(this.element).empty().remove();
107             }
108 
109             this.base();
110         },
111 
112         /**
113          * Release the object back into the object pool.
114          */
115         release:function () {
116             this.base();
117             this.element = null;
118             this.events = null;
119             this.eventListeners = null;
120             this.jQObject = undefined;
121         },
122 
123         /**
124          * Set the element which will represent this object within
125          * its rendering context.
126          *
127          * @param element {HTMLElement} The HTML element this object is associated with.
128          */
129         setElement:function (element) {
130             // If it isn't an element (prolly jQuery) then get the root element in the jQuery object
131             this.element = element.nodeType ? element : element[0];
132         },
133 
134         /**
135          * Get the element which represents this object within its rendering context.
136          *
137          * @return {HTMLElement} The HTML element
138          */
139         getElement:function () {
140             return this.element;
141         },
142 
143         /**
144          * A helper method to provide access to the jQuery object wrapping the
145          * element for this object.  This allows direct access to the DOM.
146          *
147          * @return {jQuery} A jQuery object
148          */
149         jQ:function () {
150             if (!this.jQObject) {
151                 this.jQObject = this.element ? $(this.element) : null;
152             }
153             return this.jQObject;
154         },
155 
156         /**
157          * Abstract update method to set the state of the object.  This method
158          * will be called each frame that is generated by the engine.  The
159          * context where the object will be rendered is passed, along with the
160          * current engine time.  Use this method to update components and
161          * perform housekeeping on the object.
162          *
163          * @param renderContext {R.rendercontexts.AbstractRenderContext} The context the object exists within
164          * @param time {Number} The current engine time, in milliseconds
165          * @param dt {Number} The delta between the world time and the last time the world was updated
166          *          in milliseconds.
167          */
168         update:function (renderContext, time, dt) {
169         },
170 
171         /**
172          * Add an event handler to this object. Within the
173          * event handler, <tt>this</tt> refers to the object upon which the event is being triggered. It is
174          * possible to bind an event simply by calling <tt>addEvent</tt> with the type and callback, like
175          * so:
176          * <pre>
177          *    this.addEvent("click", function(evt) {
178        *       this.doSomething(evt);
179        *    });
180          * </pre>
181          * However, if you need to reference another object during the binding process, such as when
182          * a render context is binding an event to a game object, you could pass a reference object
183          * as the first argument:
184          * <pre>
185          *    // "this" refers to the render context
186          *    someObj.addEvent(this, "click", function(evt) {
187        *       // Inside the handler, "this" is the target of the event
188        *       this.doSomething(evt);
189        *    });
190          * </pre>
191          * The purpose behind this is that if the render context assigned the event, it should
192          * probably remove the handler, rather than the game object needing to remove the handler.
193          * But, if the game object <i>also</i> has a "click" handler, you don't want to remove
194          * <i>that handler</i> since the game object may still need it.
195          * <p/>
196          * If the event handler explicitly returns <code>false</code> (not <code>null</code>, or
197          * <code>undefined</code>), further event handlers will not be processed.
198          *
199          * @param [ref] {Object} The object reference which is assigning the event
200          * @param type {String} The event type to respond to
201          * @param [data] {Array} Optional data to pass to the handler when it is invoked.
202          * @param fn {Function} The function to trigger when the event fires
203          */
204         addEvent:function (ref, type, data, fn) {
205 
206             // CAUTION: Brain Teaser
207             fn = R.isString(ref) ? (R.isFunction(type) ? type : (R.isFunction(data) ? data : fn)) :
208                 R.isFunction(data) ? data : fn;
209             data = R.isString(ref) ? (R.isFunction(type) ? null : (R.isFunction(data) ? null : data)) :
210                 R.isFunction(data) ? null : data;
211             type = R.isString(ref) ? ref : type;
212             ref = R.isString(ref) ? this : ref;
213             // CAUTION -------------
214 
215             if (ref == null) {
216                 // This is a global assignment to the document body.  Many listeners
217                 // may collect data from the event handler.
218                 R.debug.Console.info("Global assignment of event '" + type + "'");
219                 R.engine.Events.setHandler(document.body, type, data || fn, fn);
220                 this.events["document," + type] = fn;
221             }
222             else {
223                 R.debug.Console.debug(ref.getName() + " attach event '" + type + "' to " + this.getName());
224                 if (this.getElement() && ref instanceof R.rendercontexts.AbstractRenderContext) {
225                     R.engine.Events.setHandler(this.getElement(), type, data || fn, fn);
226 
227                     // Remember the handler by the reference object's name and event type
228                     this.events[ref.getName() + "," + type] = fn;
229                 } else {
230                     // We want to be able to add event handlers to objects which don't
231                     // have an element associated with them as well
232                     var listeners = this.eventListeners[type.toUpperCase()];
233                     if (!listeners) {
234                         listeners = this.eventListeners[type.toUpperCase()] = [];
235                     }
236 
237                     // Add the new listener
238                     listeners.push({
239                         id:ref.getName() + "," + type,
240                         data:data,
241                         callback:fn
242                     });
243                 }
244             }
245         },
246 
247         /**
248          * Helper method to simplify wiring event handlers to event types.  You can assign
249          * multiple handlers, by name, using object or array notation, for example:
250          * <pre>
251          *    // Shorthand
252          *    this.addEvents({
253        *       "keydown": function(evt, which) {
254        *          this.onKeyDown(which);
255        *       },
256        *       "keyup": function(evt, which) {
257        *          this.onKeyUp(which);
258        *       }
259        *    });
260          *
261          *    // Super shorthand
262          *    this.addEvents(["onKeyDown", "onKeyUp"]);
263          * </pre>
264          * Object notation allows you to still be explicit in which handler method to
265          * apply or call, and the arguments which are handled.  Array notation will
266          * simply create a method call which takes the names of the event handlers,
267          * removes "on" and then lower cases the remainder to create the event assignment.
268          * <p/>
269          * In the example above, <tt>onKeyDown</tt> is the name of your method to be
270          * called, and <tt>keydown</tt> will be the name of the actual event.
271          *
272          * @param handlers {Object|Array} The event assignments
273          */
274         addEvents:function (handlers) {
275             var self = this;
276             if ($.isArray(handlers)) {
277                 for (var h = 0; h < handlers.length; h++) {
278                     var method = this[handlers[h]];
279                     this.addEvent(handlers[h].substr(2).toLowerCase(), method);
280                 }
281             } else {
282                 $.each(handlers, function (key, value) {
283                     self.addEvent(key, value);
284                 });
285             }
286         },
287 
288         /**
289          * Remove the event handler assigned to the object for the given type.  The optional
290          * <tt>ref</tt> argument is used when another object assigned the event handler, such as:
291          * <pre>
292          *    // Handler #1
293          *    someObject.addEvent("click", function(evt) {
294        *       this.doSomething(evt);
295        *    });
296          *
297          *    // Handler #2
298          *    someObject.addEvent(anotherObject, "click", function(evt) {
299        *       this.doSomething(evt);
300        *    });
301          * </pre>
302          * You would remove the "click" handler that <tt>anotherObject</tt> assigned (handler #2),
303          * and not one that was bound by <tt>someObject</tt> (handler #1):
304          * <pre>
305          *    someObject.removeEvent(anotherObject, "click");
306          * </pre>
307          *
308          * @param [ref] {Object} The object reference which assigned the event
309          * @param type {String} The event type to remove
310          */
311         removeEvent:function (ref, type) {
312             var fn;
313             ref = R.isString(ref) ? this : ref;
314             if (ref == null) {
315                 // This was a global assignment to the document body.  Clean it up
316                 R.debug.Console.info("Global event '" + type + "' removed");
317                 fn = this.events["document," + type];
318                 R.engine.Events.clearHandler(document.body, type, fn);
319             }
320             else {
321                 R.debug.Console.info(ref.getName() + " remove event '" + type + "' from " + this.getName());
322                 var id = ref.getName() + "," + type;
323                 if (this.getElement() && ref instanceof R.rendercontexts.AbstractRenderContext) {
324                     // Find the handler to remove
325                     fn = this.events[id];
326                     if (fn) {
327                         R.engine.Events.clearHandler(this.getElement(), type, fn);
328                     }
329                     // Remove the reference
330                     delete this.events[ref.getName() + "," + type];
331                 } else {
332                     var listeners = this.eventListeners[type.toUpperCase()];
333                     if (listeners) {
334                         listeners = R.engine.Support.filter(listeners, function (e) {
335                             return e.id !== id;
336                         });
337                     }
338                 }
339             }
340         },
341 
342         /**
343          * Trigger an event on the object.  Event handlers assigned with
344          * {@link #addEvent} will be triggered and passed the event object
345          * and the data.  The scope of the listener function will be the
346          * object.
347          *
348          * @param eventName {String} The event to trigger
349          * @param [eventObj] {Event} The original event object
350          * @param data {Array} An array of data to pass to the event handler
351          */
352         triggerEvent:function (eventName, eventObj, data) {
353             var ret;
354             if (this.getElement() && this instanceof R.rendercontexts.AbstractRenderContext) {
355                 if (eventObj && (eventObj instanceof Event)) {
356                     ret = this.jQ().trigger(eventObj);
357                 } else {
358                     ret = this.jQ().trigger(eventName, eventObj);
359                 }
360                 return ret
361             } else {
362                 if (this.eventListeners) {
363                     var listeners = this.eventListeners[eventName.toUpperCase()];
364                     if (listeners) {
365                         if (eventObj && R.isArray(eventObj)) {
366                             data = eventObj;
367                             eventObj = $.Event({ type:eventName });
368                         }
369                         data = data || [];
370 
371                         // Make sure the first element is the event
372                         data.unshift(eventObj);
373 
374                         for (var e = 0; e < listeners.length; e++) {
375                             var listener = listeners[e];
376 
377                             // Append the predefined listener data to the data object
378                             if (listener.data) {
379                                 data = data.concat(listener.data);
380                             }
381 
382                             // Call the listener
383                             ret = listener.callback.apply(this, data);
384                             if (ret === false) {
385                                 return false;
386                             }
387                         }
388                     }
389                 }
390             }
391         }
392 
393     }, /** @scope R.engine.BaseObject.prototype */ {
394 
395         /**
396          * Get the class name of this object
397          *
398          * @return {String} "R.engine.BaseObject"
399          */
400         getClassName:function () {
401             return "R.engine.BaseObject";
402         }
403 
404     });
405 
406 };