/* ****************************** Module - Chat Room Author: Jack Lukic Notes: First Commit Aug 8, 2012 Designed as a simple modular chat component ****************************** */ ;(function ($, window, document, undefined) { $.fn.chatroom = function(parameters) { var settings = $.extend(true, {}, $.fn.chatroom.settings, parameters), className = settings.className, namespace = settings.namespace, selector = settings.selector, error = settings.error, // hoist arguments moduleArguments = arguments || false ; $(this) .each(function() { var $module = $(this), $expandButton = $module.find(selector.expandButton), $userListButton = $module.find(selector.userListButton), $userList = $module.find(selector.userList), $room = $module.find(selector.room), $userCount = $module.find(selector.userCount), $log = $module.find(selector.log), $message = $module.find(selector.message), $messageInput = $module.find(selector.messageInput), $messageButton = $module.find(selector.messageButton), instance = $module.data('module'), html = '', users = {}, channel, loggedInUser, message, count, height, pusher, module ; module = { width: { log : $log.width(), userList : $userList.outerWidth() }, initialize: function() { // check error conditions if(Pusher === undefined) { module.error(error.pusher); } if(settings.key === undefined || settings.channelName === undefined) { module.error(error.key); return false; } else if( !(settings.endpoint.message || settings.endpoint.authentication) ) { module.error(error.endpoint); return false; } // define pusher pusher = new Pusher(settings.key); Pusher.channel_auth_endpoint = settings.endpoint.authentication; channel = pusher.subscribe(settings.channelName); channel.bind('pusher:subscription_succeeded', module.user.list.create); channel.bind('pusher:subscription_error', module.error); channel.bind('pusher:member_added', module.user.joined); channel.bind('pusher:member_removed', module.user.left); channel.bind('update_messages', module.message.receive); $.each(settings.customEvents, function(label, value) { channel.bind(label, value); }); // bind module events $userListButton .on('click.' + namespace, module.event.toggleUserList) ; $expandButton .on('click.' + namespace, module.event.toggleExpand) ; $messageInput .on('keydown.' + namespace, module.event.input.keydown) .on('keyup.' + namespace, module.event.input.keyup) ; $messageButton .on('mouseenter.' + namespace, module.event.hover) .on('mouseleave.' + namespace, module.event.hover) .on('click.' + namespace, module.event.submit) ; // scroll to bottom of chat log $log .animate({ scrollTop: $log.prop('scrollHeight') }, 400) ; $module .data('module', module) .addClass(className.loading) ; }, // refresh module refresh: function() { // reset width calculations $userListButton .removeClass(className.active) ; module.width = { log : $log.width(), userList : $userList.outerWidth() }; if( $userListButton.hasClass(className.active) ) { module.user.list.hide(); } $module.data('module', module); }, user: { updateCount: function() { if(settings.userCount) { users = $module.data('users'); count = 0; $.each(users, function() { count++; }); $userCount .html( settings.templates.userCount(count) ) ; } }, // add user to user list joined: function(member) { users = $module.data('users'); if(member.id != 'anonymous' && users[ member.id ] === undefined ) { users[ member.id ] = member.info; if(settings.randomColor && member.info.color === undefined) { member.info.color = settings.templates.color(member.id); } html = settings.templates.userList(member.info); if(member.info.isAdmin) { $(html) .prependTo($userList) ; } else { $(html) .appendTo($userList) ; } if(settings.partingMessages) { $log .append( settings.templates.joined(member.info) ) ; module.message.scroll.test(); } module.user.updateCount(); } }, // remove user from user list left: function(member) { users = $module.data('users'); if(member !== undefined && member.id !== 'anonymous') { delete users[ member.id ]; $module .data('users', users) ; $userList .find('[data-id='+ member.id + ']') .remove() ; if(settings.partingMessages) { $log .append( settings.templates.left(member.info) ) ; module.message.scroll.test(); } module.user.updateCount(); } }, list: { // receives list of members and generates user list create: function(members) { users = {}; members.each(function(member) { if(member.id !== 'anonymous' && member.id !== 'undefined') { if(settings.randomColor && member.info.color === undefined) { member.info.color = settings.templates.color(member.id); } // sort list with admin first html = (member.info.isAdmin) ? settings.templates.userList(member.info) + html : html + settings.templates.userList(member.info) ; users[ member.id ] = member.info; } }); $module .data('users', users) .data('user', users[members.me.id] ) .removeClass(className.loading) ; $userList .html(html) ; module.user.updateCount(); $.proxy(settings.onJoin, $userList.children())(); }, // shows user list show: function() { $log .animate({ width: (module.width.log - module.width.userList) }, { duration : settings.speed, easing : settings.easing, complete : module.message.scroll.move }) ; }, // hides user list hide: function() { $log .stop() .animate({ width: (module.width.log) }, { duration : settings.speed, easing : settings.easing, complete : module.message.scroll.move }) ; } } }, message: { // handles scrolling of chat log scroll: { test: function() { height = $log.prop('scrollHeight') - $log.height(); if( Math.abs($log.scrollTop() - height) < settings.scrollArea) { module.message.scroll.move(); } }, move: function() { height = $log.prop('scrollHeight') - $log.height(); $log .scrollTop(height) ; } }, // sends chat message send: function(message) { if( !module.utils.emptyString(message) ) { $.api({ url : settings.endpoint.message, method : 'POST', data : { 'message': { content : message, timestamp : new Date().getTime() } } }); } }, // receives chat response and processes receive: function(response) { message = response.data; users = $module.data('users'); loggedInUser = $module.data('user'); if(users[ message.userID] !== undefined) { // logged in user's messages already pushed instantly if(loggedInUser === undefined || loggedInUser.id != message.userID) { message.user = users[ message.userID ]; module.message.display(message); } } }, // displays message in chat log display: function(message) { $log .append( settings.templates.message(message) ) ; module.message.scroll.test(); $.proxy(settings.onMessage, $log.children().last() )(); } }, expand: function() { $module .addClass(className.expand) ; $.proxy(settings.onExpand, $module )(); module.refresh(); }, contract: function() { $module .removeClass(className.expand) ; $.proxy(settings.onContract, $module )(); module.refresh(); }, event: { input: { keydown: function(event) { if(event.which == 13) { $messageButton .addClass(className.down) ; } }, keyup: function(event) { if(event.which == 13) { $messageButton .removeClass(className.down) ; module.event.submit(); } } }, // handles message form submit submit: function() { var message = $messageInput.val(), loggedInUser = $module.data('user') ; if(loggedInUser !== undefined && !module.utils.emptyString(message)) { module.message.send(message); // display immediately module.message.display({ user: loggedInUser, text: message }); module.message.scroll.move(); $messageInput .val('') ; } }, // handles button click on expand button toggleExpand: function() { if( !$module.hasClass(className.expand) ) { $expandButton .addClass(className.active) ; module.expand(); } else { $expandButton .removeClass(className.active) ; module.contract(); } }, // handles button click on user list button toggleUserList: function() { if( !$log.is(':animated') ) { if( !$userListButton.hasClass(className.active) ) { $userListButton .addClass(className.active) ; module.user.list.show(); } else { $userListButton .removeClass('active') ; module.user.list.hide(); } } } }, utils: { emptyString: function(string) { if(typeof string == 'string') { return (string.search(/\S/) == -1); } return false; } }, setting: function(name, value) { if(value !== undefined) { if( $.isPlainObject(name) ) { $.extend(true, settings, name); } else { settings[name] = value; } } else { return settings[name]; } }, internal: function(name, value) { if(value !== undefined) { if( $.isPlainObject(name) ) { $.extend(true, module, name); } else { module[name] = value; } } else { return module[name]; } }, debug: function() { if(settings.debug) { if(settings.performance) { module.performance.log(arguments); } else { module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); module.debug.apply(console, arguments); } } }, verbose: function() { if(settings.verbose && settings.debug) { if(settings.performance) { module.performance.log(arguments); } else { module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); module.verbose.apply(console, arguments); } } }, error: function() { module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); module.error.apply(console, arguments); }, performance: { log: function(message) { var currentTime, executionTime, previousTime ; if(settings.performance) { currentTime = new Date().getTime(); previousTime = time || currentTime; executionTime = currentTime - previousTime; time = currentTime; performance.push({ 'Element' : element, 'Name' : message[0], 'Arguments' : [].slice.call(message, 1) || '', 'Execution Time' : executionTime }); } clearTimeout(module.performance.timer); module.performance.timer = setTimeout(module.performance.display, 100); }, display: function() { var title = settings.name + ':', totalTime = 0 ; time = false; clearTimeout(module.performance.timer); $.each(performance, function(index, data) { totalTime += data['Execution Time']; }); title += ' ' + totalTime + 'ms'; if(moduleSelector) { title += ' \'' + moduleSelector + '\''; } title += ' ' + '(' + $allDropdowns.size() + ')'; if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { console.groupCollapsed(title); if(console.table) { console.table(performance); } else { $.each(performance, function(index, data) { console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); }); } console.groupEnd(); } performance = []; } }, invoke: function(query, passedArguments, context) { var maxDepth, found ; passedArguments = passedArguments || queryArguments; context = element || context; if(typeof query == 'string' && instance !== undefined) { query = query.split(/[\. ]/); maxDepth = query.length - 1; $.each(query, function(depth, value) { if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) { instance = instance[value]; } else if( instance[value] !== undefined ) { found = instance[value]; } else { module.error(error.method); } }); } if ( $.isFunction( found ) ) { return found.apply(context, passedArguments); } return found || false; } }; if(methodInvoked) { if(instance === undefined) { module.initialize(); } invokedResponse = module.invoke(query); } else { if(instance !== undefined) { module.destroy(); } module.initialize(); } }) ; return (invokedResponse) ? invokedResponse : this ; }; $.fn.chatroom.settings = { name : 'Chat', debug : false, namespace : 'chat', channel : 'present-chat', onJoin : function(){}, onMessage : function(){}, onExpand : function(){}, onContract : function(){}, customEvents : {}, partingMessages : false, userCount : true, randomColor : true, speed : 300, easing : 'easeOutQuint', // pixels from bottom of chat log that should trigger auto scroll to bottom scrollArea : 9999, endpoint : { message : false, authentication : false }, error: { method : 'The method you called is not defined', endpoint : 'Please define a message and authentication endpoint.', key : 'You must specify a pusher key and channel.', pusher : 'You must include the Pusher library.' }, className : { expand : 'expand', active : 'active', hover : 'hover', down : 'down', loading : 'loading' }, selector : { userCount : '.actions .message', userListButton : '.actions .list.button', expandButton : '.actions .expand.button', room : '.room', userList : '.room .list', log : '.room .log', message : '.room .log .message', author : '.room log .message .author', messageInput : '.talk input', messageButton : '.talk .send.button' }, templates: { userCount: function(number) { return number + ' users in chat'; }, color: function(userID) { var colors = [ '#000000', '#333333', '#666666', '#999999', '#CC9999', '#CC6666', '#CC3333', '#993333', '#663333', '#CC6633', '#CC9966', '#CC9933', '#999966', '#CCCC66', '#99CC66', '#669933', '#669966', '#33A3CC', '#336633', '#33CCCC', '#339999', '#336666', '#336699', '#6666CC', '#9966CC', '#333399', '#663366', '#996699', '#993366', '#CC6699' ] ; return colors[ Math.floor( Math.random() * colors.length) ]; }, message: function(message) { var html = '' ; if(message.user.isAdmin) { message.user.color = '#55356A'; html += '