1 /*
  2 Copyright 2011 Philip Schweiger <pschwei1@gmail.com>
  3 
  4 This program is free software: you can redistribute it and/or modify
  5 it under the terms of the GNU General Public License as published by
  6 the Free Software Foundation, either version 3 of the License, or
  7 (at your option) any later version.
  8 
  9 This program is distributed in the hope that it will be useful,
 10 but WITHOUT ANY WARRANTY; without even the implied warranty of
 11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 12 GNU General Public License for more details.
 13 
 14 You should have received a copy of the GNU General Public License
 15 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 16 */
 17 
 18 /**
 19  * @class FSW - wrapper for the Facebook JavaScript SDK.
 20  * (not endorsed by or affiliated with Facebook).
 21  * @version 0.2.0
 22  * @author Philip Schweiger [pschwei1@gmail.com]
 23  */
 24 var fsw = (function () {
 25 
 26     /**
 27      * Store basic information about the application
 28      * @ignore
 29      */
 30 	var _appId,_canvasUrl,_tabUrl,
 31 	
 32 	/**
 33 	* Check if a value exists in an array
 34 	* @ignore
 35 	*/
 36 	inArray = function(val, array){
 37 		for ( var i = 0, length = array.length; i < length; i++ ) {
 38 			if ( array[i] === val) {
 39 				return true;
 40 			}
 41 		}
 42 		return false;
 43 	},
 44 	
 45 	/**
 46 	* Trim whitespace from start and end of string
 47 	* @ignore
 48 	*/
 49 	trim = function(string) {
 50 		return string.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
 51 	},
 52 
 53 	/**
 54 	* Add xmlns:fb to <html> to support rendering of xfbml elements
 55 	* @ignore
 56 	*/
 57 	setXfbml = function(){
 58 		var atts = document.documentElement.attributes,
 59 			fb = document.createAttribute('xmlns:fb');
 60 		fb.nodeValue = 'https://www.facebook.com/2008/fbml';
 61 		atts.setNamedItem(fb);
 62 	};
 63 	setXfbml();
 64    
 65     return {
 66 		/**
 67 		 * Specify a function to execute when the the FB JS SDK is fully loaded.
 68 		 * Wrap all "fsw" methods in this to avoid a race condition.
 69 		 * @example fsw.ready(function(){
 70 		 *     fsw.init(appId);
 71 		 *     fsw.doSomething(et cetera);
 72 		 * });
 73 		 */
 74         ready: function(callback) { 
 75 			 var head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement;
 76 				script = document.createElement('script');
 77 				loaded = false,
 78 			script.async = 'async';
 79 			script.src = 'https://connect.facebook.net/en_US/all.js';
 80 			//IE uses onreadystate, normal browsers use onload
 81 			script.onload = script.onreadystatechange = function() {
 82 				if (script.readyState) {
 83 					if (script.readyState == 'loaded' &&  !loaded) {
 84 						loaded = true;
 85 						callback();
 86 					}
 87 				} else {
 88 					callback();
 89 				}
 90 			}
 91 			head.insertBefore( script, head.firstChild );
 92         },
 93 
 94 		/**
 95 		 * Calls FB.init() and stores basic app info for later retrieval.
 96 		 *
 97 		 * @memberOf fsw
 98 		 * @param {String} appId application ID
 99 		 * @param {String} [canvasUrl] Fully-qualified app canvas URL
100 		 * @param {String} [tabUrl] Fully-qualified app tab URL
101 		 */
102         init:function(appId, canvasUrl, tabUrl){
103             if (typeof appId === 'undefined') {
104                 throw new Error('fsw.init(): Missing appId');
105             }
106 		
107             _appId = appId;
108             _canvasUrl = (typeof canvasUrl === 'undefined')? null : canvasUrl;
109             _tabUrl = (typeof tabUrl === 'undefined')? null : tabUrl;
110 			
111             FB.init({appId:_appId,status:true,cookie:true,xfbml:true,oauth:true});
112 		},
113         
114 		/**
115 		* Get Application ID
116 		* @memberOf fsw
117 		* @returns {String} application id
118 		*/
119         getAppId: function(){
120             return _appId;
121         },
122 
123 		/**
124 		* Get Canvas URL
125 		* Returns null if none set.
126 		* @memberOf fsw
127 		* @returns {String} canvas url
128 		*/
129         getCanvasUrl: function(){
130             return _canvasUrl;
131         },
132 
133 		/**
134 		* Get tab URL
135 		* Returns null if none set.
136 		* @memberOf fsw
137 		* @returns {String} tab url
138 		*/
139         getTabUrl: function(){
140             return _tabUrl;
141         },
142 
143 		/**
144 		* @class Authorization module. Handles login and permissions.
145 		* @name fsw.auth
146 		*/   
147 		auth: {
148 			/**
149 			* Bind FB.login() to a specified DOM element
150 			* Only binds to elements that have an ID.
151 			* @memberOf fsw.auth
152 			* @param {Strging} triggerId ID of element to bind to
153 			* @param {Function} callback function to launch after login
154 			* @param {String} [permissions] Comma separated list, 
155 			* see http://bit.ly/hOUAQD and http://bit.ly/cjEOKB
156 			*/
157 			createBtn: function(triggerId,callback, permissions){
158 				var trigger = document.getElementById(triggerId),
159 					perms = (typeof permissions !== 'undefined')? {perms:permissions}: null;
160 				if (!trigger) {
161 					throw new Error('fsw.auth.createBtn: No element with ID "'+triggerId+'" exists in DOM');
162 				}
163 
164 				if (typeof callback !== 'function') {
165 					throw new Error('fsw.auth.createBtn: must pass a callback function');
166 				}
167 
168 				trigger.onclick = function(){
169 				   FB.login(callback,perms);
170 				   return false;
171 				};
172 
173 			}
174 		},
175    
176 		/**
177 		* @class Like module. Handles the like button and related events.
178 		* @name fsw.like
179 		*/
180 		like: {
181 			/**
182 			* Constructor for likeBtn instances
183 			* @memberOf fsw.like
184 			* @private
185 			* @returns {LikeBtn}
186 			*/
187 			likeBtn: function(container, target, opts, layoutOpts){
188 				var action = (opts.action === 'recommend')? 'recommend' : 'like', 
189 					colorscheme = (opts.colorscheme === 'dark')? 'dark' : 'light',
190 					faces = opts.faces === true,
191 					font = opts.font || 'arial',
192 					layout = (inArray(opts.layout,layoutOpts))? opts.layout : 'standard',
193 					ref = (typeof opts.ref === 'string')? opts.ref: '',
194 					send = opts.send === true,
195 					width = (parseInt(opts.width,10) > 0)? parseInt(opts.width,10) : 450,
196 					
197 					fblike = document.createElement('fb:like');
198 				
199 				fblike.setAttribute('href',target);
200 				fblike.setAttribute('action',action);
201 				fblike.setAttribute('send',send);
202 				fblike.setAttribute('width',width);
203 				fblike.setAttribute('show_faces',faces);
204 				fblike.setAttribute('font',font);
205 				fblike.setAttribute('layout',layout);
206 				fblike.setAttribute('colorscheme',colorscheme);
207 				fblike.setAttribute('ref',ref);
208 				
209 				container.appendChild(fblike);
210 
211 				FB.XFBML.parse();
212 			  
213 				return this;
214 			},
215 		
216 			/**
217 			* Create a FB "like" button
218 			* Inserts a like button in the specified DOM element.
219 			* Containing element must have in ID.
220 			* @memberOf fsw.like
221 			* @param {String} containerId ID of the DOM element in which to insert the Like button
222 			* @param {String} target URL to Like
223 			* @param [options] Additional information passed to the like button
224 			* @param {String} options.action Default: like - Alts: recommend
225 			* @param {String} options.colorscheme Default: light - Alts: dark
226 			* @param {Boolean} options.faces Default: false - Show fans' profile pics
227 			* @param {String} options.font Default: arial - Alts: lucida grande | segoe ui | tahoma | trebuchet ms | verdana
228 			* @param {String} options.layout Default: standard - Alts: button_count | box_count
229 			* @param {String} options.ref a label for tracking referrals
230 			* @param {Boolean} options.send Default: false - Include a "send" button?
231 			* @param {Number} options.width Default: 420 - Width, in pixels, of plugin
232 			*/
233 			createBtn: function(containerId, target, options) {
234 				var container = document.getElementById(containerId),
235 					err = new Error(),
236 					opts = options || {},
237 					layoutOpts = ['standard','button_count','box_count'];
238 
239 				if (!container) {
240 					throw new Error('fsw.like.createBtn: No element with ID "'+containerId+'" exists in DOM');
241 				}
242 					
243 				if (target === undefined) {
244 				   err.message = 'fws.like.createBtn(): Missing "like" target';
245 				   throw err;
246 				}
247 			   return new this.likeBtn(container, target, opts, layoutOpts);
248 			}
249 		},
250    
251 		/**
252 		* @class Send module. Handles the send button and related events.
253 		* @name fsw.send
254 		*/
255 		send: {
256 			/**
257 			* Constructor for sendBtn instances
258 			* @memberOf fsw.send
259 			* @private
260 			* @returns {SendBtn}
261 			*/
262 			sendBtn: function(container, target, opts){
263 
264 				var colorscheme = (opts.colorscheme === 'dark')? 'dark' : 'light',
265 					font = opts.font || 'arial',
266 					ref = (typeof opts.ref === 'string')? opts.ref : '',
267 					
268 					fbsend = document.createElement('fb:send');
269 				
270 				fbsend.setAttribute('href',target);
271 				fbsend.setAttribute('font',font);
272 				fbsend.setAttribute('colorscheme',colorscheme);
273 				fbsend.setAttribute('ref',ref);
274 				
275 				container.appendChild(fbsend);
276 
277 				FB.XFBML.parse();
278 				
279 				return this;
280 			},    
281 			
282 			/**
283 			* Create a FB "send" button
284 			* Inserts a send button in the specified DOM element.
285 			* Containing element must have in ID.
286 			* @memberOf fsw.send
287 			* @param {String} containerId ID of the DOM element in which to insert the Send button
288 			* @param {String} target URL to Like
289 			* @param [options] Additional information passed to the like button
290 			* @param {String} options.colorscheme Default: light - Alts: dark
291 			* @param {String} options.font Default: arial - Alts: lucida grande | segoe ui | tahoma | trebuchet ms | verdana
292 			* @param {String} options.ref a label for tracking referrals
293 			*/
294 			createBtn: function(containerId, target, options) {
295 				var container = document.getElementById(containerId),
296 					err = new Error(),
297 					opts = options || {};
298 
299 				if (!container) {
300 					throw new Error('fsw.send.createBtn: No element with ID "'+containerId+'" exists in DOM');
301 				}
302 
303 				if (target === undefined) {
304 				   err.message = 'fsw.send.createBtn(): Missing "send" target';
305 				   throw err;
306 				}
307 
308 				return new this.sendBtn(container, target, opts);
309 			}
310 		},
311 
312 		/**
313 		* @class Stream module. Handles stream interactions, such as posting to Wall.
314 		* @name fsw.stream
315 		*/
316 		stream: {
317 			/**
318 			* Post to current user's wall/stream
319 			* @memberOf fsw.stream
320 			* @param {Object} [opts] Options for customizing the wall post
321 			* @param {String} [opts.title] Title of the post. Defaults to blank.
322 			* @param {String} [opts.subtitle] Subtitle of the post
323 			* @param {String} [opts.link] The link attached to this post. 
324 			* Must be within your app's domain.
325 			* @param {String} [opts.description] App-supplied text to appear in the post. 
326 			* Defaults to blank. If no link and no media, description will not appear.
327 			* @param {String} [opts.media] URL of media to display with the post. FB 
328 			* will try to pull an image from linked page if this is missing. If you pass a swf, you must also pass a thumbnail image in comma-separated format, eg "http://domain/movie.swf, http://domain/image.jpg"
329 			* @param {String} [opts.action] Comma-separated pair of verb and link, 
330 			* eg "action,http://www.example.com" Link must be within your app's domain, 
331 			* and name becomes lowercase alphanumeric.
332 			* @param {Function} [callback] Function to call after user sends or cancels 
333 			* post. Returns null on user cancel, post id on user send.
334 			*/    
335 			post: function(options, callback){
336 				var opts = options || {},
337 					name = opts.title || ' ',
338 					cap = opts.subtitle || ' ',
339 					link = opts.link || null,
340 					source = opts.media || null,
341 					desc = opts.description || ' ',
342 					action = (function(){
343 					  if (opts.action) {
344 						  var action = opts.action.split(',');
345 						  return {name:action[0],link:trim(action[1])};
346 					  } else {
347 						  return null;
348 					  }
349 					}()),
350 					cb = (typeof callback === 'function')? callback : null,
351 					
352 					fbparams = {
353 						actions:action,
354 						caption:cap,
355 						description:desc,
356 						link:link,
357 						method:'feed',
358 						name:name,
359 						source:source
360 					};
361 				//If this is a swf, set a thumbnail
362 				if (source && source.indexOf('.swf') > -1) {
363 				
364 					if (source.indexOf(',') === -1) {
365 						throw new Error('fsw.stream.post(): Missing thumbnail for swf');
366 					} else {
367 						var parts = source.split(',');
368 						fbparams.source = parts[0];
369 						fbparams.picture = trim(parts[1]);
370 					}
371 				}
372 				
373 				FB.ui(fbparams,cb);
374 			}  
375 		},
376 
377 		/**
378 		* @class Request module. Sends app requests.
379 		* @name fsw.request
380 		*/
381 		request: {
382 			/**
383 			*Send an app request to a specific friend, or to several friends. 
384 			*Note that this module does not address server-side handling of app requests.
385 			*@memberOf fsw.request
386 			*@param {Object} opts
387 			*@param {String} opts.message Request message
388 			*@param {String} [opts.to] A FBID or FB user name to send the request to. Must be a friend of the user.
389 			*@param {String} [opts.data] Data to send along with the request. Better explanation TK.
390 			*@param {Function}[callback] Function to call after user sends or cancels post. Returns null on user cancel, request id on user send.
391 			*/
392 			send: function(opts, callback){
393 				if (typeof opts.message !== 'string') {
394 					throw new Error('Must pass opts.message as a string');
395 				}
396 					
397 				var data = opts.data || null,
398 					to = opts.to || null;
399 					
400 				callback = (typeof callback === 'function')? callback : null;
401 					
402 				FB.ui(
403 					{
404 						data:data,
405 						message:opts.message,
406 						method:'apprequests',
407 						to:to
408 					},
409 					callback
410 				);
411 			}  
412 		}
413 	}
414     
415 }());
416 
417 /**
418 * Bind to "like" buttons' like/unlike events
419 * @memberOf fsw.like
420 * @param {Function} onLike callback when user "likes" using FB like button
421 * @param {Function} onUnlike callback when user "unlikes" using FB like button
422 * 
423 * @example var myButton = fsw.like.createBtn(containerId,target); 
424 * myButton.bind(onLike, onUnlike);
425 */
426 fsw.like.likeBtn.prototype.bind = function(onLike,onUnlike){
427         
428 	if (typeof onLike !== 'function' || typeof onUnlike !== 'function') {
429 		throw new Error("must pass an onLike and an onUnlike callback as function");
430 	}
431 	
432 	FB.Event.subscribe('edge.create',onLike);
433 	FB.Event.subscribe('edge.remove',onUnlike);
434 	
435 };
436 
437 /**
438 * Bind to "send" buttons' like/unlike events
439 * @memberOf fsw.send
440 * @param {Function} onSend callback when user "sends" using FB send button
441 * 
442 * @example var myButton = fsw.send.createBtn(containerId,target); 
443 * myButton.bind(onSend);
444 */
445 fsw.send.sendBtn.prototype.bind = function(onSend){
446         
447 	if (typeof onSend !== 'function') {
448 		throw new Error("must pass an onSend callback as function");
449 	}
450 
451 	FB.Event.subscribe('message.send',onSend);
452 };
453 
454 /**
455  * On <body> load, create and insert #fb-root into the DOM. Don't wait for init();
456  * Note - I'm a bit worried this sets up a race condition against the FB libary loading, but
457  * in nearly all cases the body will load much faster than the FB library will, so I _think_ we're ok.
458  * @ignore
459  */
460  fsw.isBodyLoaded = function(callback) {
461 	var body = document.getElementsByTagName('body');
462 	if (body.length > 0) {
463 		var fbroot = document.createElement('div');
464 		fbroot.id = 'fb-root';
465 		body[0].appendChild(fbroot);
466 	} else {
467 		setTimeout(fsw.isBodyLoaded,1,callback);
468 	}
469 }
470 fsw.isBodyLoaded();