1 /*
  2 Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
  3 For licensing, see LICENSE.html or http://ckeditor.com/license
  4 */
  5 
  6 /**
  7  * @fileOverview Defines the {@link CKEDITOR.event} class, which serves as the
  8  *		base for classes and objects that require event handling features.
  9  */
 10 
 11 if ( !CKEDITOR.event )
 12 {
 13 	/**
 14 	 * Creates an event class instance. This constructor is rearely used, being
 15 	 * the {@link #.implementOn} function used in class prototypes directly
 16 	 * instead.
 17 	 * @class This is a base class for classes and objects that require event
 18 	 * handling features.<br />
 19 	 * <br />
 20 	 * Do not confuse this class with {@link CKEDITOR.dom.event} which is
 21 	 * instead used for DOM events. The CKEDITOR.event class implements the
 22 	 * internal event system used by the CKEditor to fire API related events.
 23 	 * @example
 24 	 */
 25 	CKEDITOR.event = function()
 26 	{};
 27 
 28 	/**
 29 	 * Implements the {@link CKEDITOR.event} features in an object.
 30 	 * @param {Object} targetObject The object into which implement the features.
 31 	 * @example
 32 	 * var myObject = { message : 'Example' };
 33 	 * <b>CKEDITOR.event.implementOn( myObject }</b>;
 34 	 * myObject.on( 'testEvent', function()
 35 	 *     {
 36 	 *         alert( this.message );  // "Example"
 37 	 *     });
 38 	 * myObject.fire( 'testEvent' );
 39 	 */
 40 	CKEDITOR.event.implementOn = function( targetObject )
 41 	{
 42 		var eventProto = CKEDITOR.event.prototype;
 43 
 44 		for ( var prop in eventProto )
 45 		{
 46 			if ( targetObject[ prop ] == undefined )
 47 				targetObject[ prop ] = eventProto[ prop ];
 48 		}
 49 	};
 50 
 51 	CKEDITOR.event.prototype = (function()
 52 	{
 53 		// Returns the private events object for a given object.
 54 		var getPrivate = function( obj )
 55 		{
 56 			var _ = ( obj.getPrivate && obj.getPrivate() ) || obj._ || ( obj._ = {} );
 57 			return _.events || ( _.events = {} );
 58 		};
 59 
 60 		var eventEntry = function( eventName )
 61 		{
 62 			this.name = eventName;
 63 			this.listeners = [];
 64 		};
 65 
 66 		eventEntry.prototype =
 67 		{
 68 			// Get the listener index for a specified function.
 69 			// Returns -1 if not found.
 70 			getListenerIndex : function( listenerFunction )
 71 			{
 72 				for ( var i = 0, listeners = this.listeners ; i < listeners.length ; i++ )
 73 				{
 74 					if ( listeners[i].fn == listenerFunction )
 75 						return i;
 76 				}
 77 				return -1;
 78 			}
 79 		};
 80 
 81 		return /** @lends CKEDITOR.event.prototype */ {
 82 			/**
 83 			 * Registers a listener to a specific event in the current object.
 84 			 * @param {String} eventName The event name to which listen.
 85 			 * @param {Function} listenerFunction The function listening to the
 86 			 *		event. A single {@link CKEDITOR.eventInfo} object instanced
 87 			 *		is passed to this function containing all the event data.
 88 			 * @param {Object} [scopeObj] The object used to scope the listener
 89 			 *		call (the this object. If omitted, the current object is used.
 90 			 * @param {Object} [listenerData] Data to be sent as the
 91 			 *		{@link CKEDITOR.eventInfo#listenerData} when calling the
 92 			 *		listener.
 93 			 * @param {Number} [priority] The listener priority. Lower priority
 94 			 *		listeners are called first. Listeners with the same priority
 95 			 *		value are called in registration order. Defaults to 10.
 96 			 * @example
 97 			 * someObject.on( 'someEvent', function()
 98 			 *     {
 99 			 *         alert( this == someObject );  // "true"
100 			 *     });
101 			 * @example
102 			 * someObject.on( 'someEvent', function()
103 			 *     {
104 			 *         alert( this == anotherObject );  // "true"
105 			 *     }
106 			 *     , anotherObject );
107 			 * @example
108 			 * someObject.on( 'someEvent', function( event )
109 			 *     {
110 			 *         alert( event.listenerData );  // "Example"
111 			 *     }
112 			 *     , null, 'Example' );
113 			 * @example
114 			 * someObject.on( 'someEvent', function() { ... } );                   // 2nd called
115 			 * someObject.on( 'someEvent', function() { ... }, null, null, 100 );  // 3rd called
116 			 * someObject.on( 'someEvent', function() { ... }, null, null, 1 );    // 1st called
117 			 */
118 			on : function( eventName, listenerFunction, scopeObj, listenerData, priority )
119 			{
120 				// Get the event entry (create it if needed).
121 				var events = getPrivate( this ),
122 					event = events[ eventName ] || ( events[ eventName ] = new eventEntry( eventName ) );
123 
124 				if ( event.getListenerIndex( listenerFunction ) < 0 )
125 				{
126 					// Get the listeners.
127 					var listeners = event.listeners;
128 
129 					// Fill the scope.
130 					if ( !scopeObj )
131 						scopeObj = this;
132 
133 					// Default the priority, if needed.
134 					if ( isNaN( priority ) )
135 						priority = 10;
136 
137 					var me = this;
138 
139 					// Create the function to be fired for this listener.
140 					var listenerFirer = function( editor, publisherData, stopFn, cancelFn )
141 					{
142 						var ev =
143 						{
144 							name : eventName,
145 							sender : this,
146 							editor : editor,
147 							data : publisherData,
148 							listenerData : listenerData,
149 							stop : stopFn,
150 							cancel : cancelFn,
151 							removeListener : function()
152 							{
153 								me.removeListener( eventName, listenerFunction );
154 							}
155 						};
156 
157 						listenerFunction.call( scopeObj, ev );
158 
159 						return ev.data;
160 					};
161 					listenerFirer.fn = listenerFunction;
162 					listenerFirer.priority = priority;
163 
164 					// Search for the right position for this new listener, based on its
165 					// priority.
166 					for ( var i = listeners.length - 1 ; i >= 0 ; i-- )
167 					{
168 						// Find the item which should be before the new one.
169 						if ( listeners[ i ].priority <= priority )
170 						{
171 							// Insert the listener in the array.
172 							listeners.splice( i + 1, 0, listenerFirer );
173 							return;
174 						}
175 					}
176 
177 					// If no position has been found (or zero length), put it in
178 					// the front of list.
179 					listeners.unshift( listenerFirer );
180 				}
181 			},
182 
183 			/**
184 			 * Fires an specific event in the object. All registered listeners are
185 			 * called at this point.
186 			 * @function
187 			 * @param {String} eventName The event name to fire.
188 			 * @param {Object} [data] Data to be sent as the
189 			 *		{@link CKEDITOR.eventInfo#data} when calling the
190 			 *		listeners.
191 			 * @param {CKEDITOR.editor} [editor] The editor instance to send as the
192 			 *		{@link CKEDITOR.eventInfo#editor} when calling the
193 			 *		listener.
194 			 * @returns {Boolean|Object} A booloan indicating that the event is to be
195 			 *		canceled, or data returned by one of the listeners.
196 			 * @example
197 			 * someObject.on( 'someEvent', function() { ... } );
198 			 * someObject.on( 'someEvent', function() { ... } );
199 			 * <b>someObject.fire( 'someEvent' )</b>;  // both listeners are called
200 			 * @example
201 			 * someObject.on( 'someEvent', function( event )
202 			 *     {
203 			 *         alert( event.data );  // "Example"
204 			 *     });
205 			 * <b>someObject.fire( 'someEvent', 'Example' )</b>;
206 			 */
207 			fire : (function()
208 			{
209 				// Create the function that marks the event as stopped.
210 				var stopped = false;
211 				var stopEvent = function()
212 				{
213 					stopped = true;
214 				};
215 
216 				// Create the function that marks the event as canceled.
217 				var canceled = false;
218 				var cancelEvent = function()
219 				{
220 					canceled = true;
221 				};
222 
223 				return function( eventName, data, editor )
224 				{
225 					// Get the event entry.
226 					var event = getPrivate( this )[ eventName ];
227 
228 					// Save the previous stopped and cancelled states. We may
229 					// be nesting fire() calls.
230 					var previousStopped = stopped,
231 						previousCancelled = canceled;
232 
233 					// Reset the stopped and canceled flags.
234 					stopped = canceled = false;
235 
236 					if ( event )
237 					{
238 						var listeners = event.listeners;
239 
240 						if ( listeners.length )
241 						{
242 							// As some listeners may remove themselves from the
243 							// event, the original array length is dinamic. So,
244 							// let's make a copy of all listeners, so we are
245 							// sure we'll call all of them.
246 							listeners = listeners.slice( 0 );
247 
248 							// Loop through all listeners.
249 							for ( var i = 0 ; i < listeners.length ; i++ )
250 							{
251 								// Call the listener, passing the event data.
252 								var retData = listeners[i].call( this, editor, data, stopEvent, cancelEvent );
253 
254 								if ( typeof retData != 'undefined' )
255 									data = retData;
256 
257 								// No further calls is stopped or canceled.
258 								if ( stopped || canceled )
259 									break;
260 							}
261 						}
262 					}
263 
264 					var ret = canceled || ( typeof data == 'undefined' ? false : data );
265 
266 					// Restore the previous stopped and canceled states.
267 					stopped = previousStopped;
268 					canceled = previousCancelled;
269 
270 					return ret;
271 				};
272 			})(),
273 
274 			/**
275 			 * Fires an specific event in the object, releasing all listeners
276 			 * registered to that event. The same listeners are not called again on
277 			 * successive calls of it or of {@link #fire}.
278 			 * @param {String} eventName The event name to fire.
279 			 * @param {Object} [data] Data to be sent as the
280 			 *		{@link CKEDITOR.eventInfo#data} when calling the
281 			 *		listeners.
282 			 * @param {CKEDITOR.editor} [editor] The editor instance to send as the
283 			 *		{@link CKEDITOR.eventInfo#editor} when calling the
284 			 *		listener.
285 			 * @returns {Boolean|Object} A booloan indicating that the event is to be
286 			 *		canceled, or data returned by one of the listeners.
287 			 * @example
288 			 * someObject.on( 'someEvent', function() { ... } );
289 			 * someObject.fire( 'someEvent' );  // above listener called
290 			 * <b>someObject.fireOnce( 'someEvent' )</b>;  // above listener called
291 			 * someObject.fire( 'someEvent' );  // no listeners called
292 			 */
293 			fireOnce : function( eventName, data, editor )
294 			{
295 				var ret = this.fire( eventName, data, editor );
296 				delete getPrivate( this )[ eventName ];
297 				return ret;
298 			},
299 
300 			/**
301 			 * Unregisters a listener function from being called at the specified
302 			 *		event. No errors are thrown if the listener has not been
303 			 *		registered previously.
304 			 * @param {String} eventName The event name.
305 			 * @param {Function} listenerFunction The listener function to unregister.
306 			 * @example
307 			 * var myListener = function() { ... };
308 			 * someObject.on( 'someEvent', myListener );
309 			 * someObject.fire( 'someEvent' );  // myListener called
310 			 * <b>someObject.removeListener( 'someEvent', myListener )</b>;
311 			 * someObject.fire( 'someEvent' );  // myListener not called
312 			 */
313 			removeListener : function( eventName, listenerFunction )
314 			{
315 				// Get the event entry.
316 				var event = getPrivate( this )[ eventName ];
317 
318 				if ( event )
319 				{
320 					var index = event.getListenerIndex( listenerFunction );
321 					if ( index >= 0 )
322 						event.listeners.splice( index, 1 );
323 				}
324 			},
325 
326 			/**
327 			 * Checks if there is any listener registered to a given event.
328 			 * @param {String} eventName The event name.
329 			 * @example
330 			 * var myListener = function() { ... };
331 			 * someObject.on( 'someEvent', myListener );
332 			 * alert( someObject.<b>hasListeners( 'someEvent' )</b> );  // "true"
333 			 * alert( someObject.<b>hasListeners( 'noEvent' )</b> );    // "false"
334 			 */
335 			hasListeners : function( eventName )
336 			{
337 				var event = getPrivate( this )[ eventName ];
338 				return ( event && event.listeners.length > 0 ) ;
339 			}
340 		};
341 	})();
342 }
343