You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

697 lines
21 KiB

  1. /* ******************************
  2. Module - Chat Room
  3. Author: Jack Lukic
  4. Notes: First Commit Aug 8, 2012
  5. Designed as a simple modular chat component
  6. ****************************** */
  7. ;(function ($, window, document, undefined) {
  8. $.fn.chat = function(key, channelName, parameters) {
  9. var
  10. settings = $.extend(true, {}, $.fn.chat.settings, parameters),
  11. // hoist arguments
  12. moduleArguments = arguments || false
  13. ;
  14. $(this)
  15. .each(function() {
  16. var
  17. $module = $(this),
  18. $expandButton = $module.find(settings.selector.expandButton),
  19. $userListButton = $module.find(settings.selector.userListButton),
  20. $userList = $module.find(settings.selector.userList),
  21. $room = $module.find(settings.selector.room),
  22. $userCount = $module.find(settings.selector.userCount),
  23. $log = $module.find(settings.selector.log),
  24. $message = $module.find(settings.selector.message),
  25. $messageInput = $module.find(settings.selector.messageInput),
  26. $messageButton = $module.find(settings.selector.messageButton),
  27. instance = $module.data('module'),
  28. className = settings.className,
  29. namespace = settings.namespace,
  30. html = '',
  31. users = {},
  32. loggedInUser,
  33. message,
  34. count,
  35. height,
  36. pusher,
  37. module
  38. ;
  39. module = {
  40. channel: false,
  41. width: {
  42. log : $log.width(),
  43. userList : $userList.outerWidth()
  44. },
  45. initialize: function() {
  46. // check error conditions
  47. if(Pusher === undefined) {
  48. module.error(settings.errors.pusher);
  49. }
  50. if(key === undefined || channelName === undefined) {
  51. module.error(settings.errors.key);
  52. return false;
  53. }
  54. else if( !(settings.endpoint.message || settings.endpoint.authentication) ) {
  55. module.error(settings.errors.endpoint);
  56. return false;
  57. }
  58. // define pusher
  59. pusher = new Pusher(key);
  60. Pusher.channel_auth_endpoint = settings.endpoint.authentication;
  61. module.channel = pusher.subscribe(channelName);
  62. module.channel.bind('pusher:subscription_succeeded', module.user.list.create);
  63. module.channel.bind('pusher:subscription_error', module.error);
  64. module.channel.bind('pusher:member_added', module.user.joined);
  65. module.channel.bind('pusher:member_removed', module.user.left);
  66. module.channel.bind('update_messages', module.message.receive);
  67. $.each(settings.customEvents, function(label, value) {
  68. module.channel.bind(label, value);
  69. });
  70. // expandable with states
  71. if( $.fn.hoverClass !== undefined && $.fn.downClass !== undefined ) {
  72. $expandButton
  73. .hoverClass()
  74. .downClass()
  75. ;
  76. $userListButton
  77. .hoverClass()
  78. .downClass()
  79. ;
  80. $messageButton
  81. .hoverClass()
  82. .downClass()
  83. ;
  84. }
  85. // bind module events
  86. $userListButton
  87. .on('click.' + namespace, module.event.toggleUserList)
  88. ;
  89. $expandButton
  90. .on('click.' + namespace, module.event.toggleExpand)
  91. ;
  92. $messageInput
  93. .on('keydown.' + namespace, module.event.input.keydown)
  94. .on('keyup.' + namespace, module.event.input.keyup)
  95. ;
  96. $messageButton
  97. .on('mouseenter.' + namespace, module.event.hover)
  98. .on('mouseleave.' + namespace, module.event.hover)
  99. .on('click.' + namespace, module.event.submit)
  100. ;
  101. // scroll to bottom of chat log
  102. $log
  103. .animate({
  104. scrollTop: $log.prop('scrollHeight')
  105. }, 400)
  106. ;
  107. $module
  108. .data('module', module)
  109. .addClass(className.loading)
  110. ;
  111. },
  112. // refresh module
  113. refresh: function() {
  114. // reset width calculations
  115. $userListButton
  116. .removeClass(className.active)
  117. ;
  118. module.width = {
  119. log : $log.width(),
  120. userList : $userList.outerWidth()
  121. };
  122. if( $userListButton.hasClass(className.active) ) {
  123. module.user.list.hide();
  124. }
  125. $module.data('module', module);
  126. },
  127. user: {
  128. updateCount: function() {
  129. if(settings.userCount) {
  130. users = $module.data('users');
  131. count = 0;
  132. $.each(users, function(index){
  133. count++;
  134. });
  135. $userCount
  136. .html( settings.templates.userCount(count) )
  137. ;
  138. }
  139. },
  140. // add user to user list
  141. joined: function(member) {
  142. users = $module.data('users');
  143. if(member.id != 'anonymous' && users[ member.id ] === undefined ) {
  144. users[ member.id ] = member.info;
  145. if(settings.randomColor && member.info.color === undefined) {
  146. member.info.color = settings.templates.color(member.id);
  147. }
  148. html = settings.templates.userList(member.info);
  149. if(member.info.isAdmin) {
  150. $(html)
  151. .prependTo($userList)
  152. .preview({
  153. type : 'user',
  154. placement : 'left'
  155. })
  156. ;
  157. }
  158. else {
  159. $(html)
  160. .appendTo($userList)
  161. .preview({
  162. type : 'user',
  163. placement : 'left'
  164. })
  165. ;
  166. }
  167. if( $.fn.preview !== undefined ) {
  168. $userList
  169. .children()
  170. .last()
  171. .preview({
  172. type: 'user',
  173. placement: 'left'
  174. })
  175. ;
  176. }
  177. if(settings.partingMessages) {
  178. $log
  179. .append( settings.templates.joined(member.info) )
  180. ;
  181. module.message.scroll.test();
  182. }
  183. module.user.updateCount();
  184. }
  185. },
  186. // remove user from user list
  187. left: function(member) {
  188. users = $module.data('users');
  189. if(member !== undefined && member.id !== 'anonymous') {
  190. delete users[ member.id ];
  191. $module
  192. .data('users', users)
  193. ;
  194. $userList
  195. .find('[data-id='+ member.id + ']')
  196. .remove()
  197. ;
  198. if(settings.partingMessages) {
  199. $log
  200. .append( settings.templates.left(member.info) )
  201. ;
  202. module.message.scroll.test();
  203. }
  204. module.user.updateCount();
  205. }
  206. },
  207. list: {
  208. // receives list of members and generates user list
  209. create: function(members) {
  210. users = {};
  211. members.each(function(member) {
  212. if(member.id !== 'anonymous' && member.id !== 'undefined') {
  213. if(settings.randomColor && member.info.color === undefined) {
  214. member.info.color = settings.templates.color(member.id);
  215. }
  216. // sort list with admin first
  217. html = (member.info.isAdmin)
  218. ? settings.templates.userList(member.info) + html
  219. : html + settings.templates.userList(member.info)
  220. ;
  221. users[ member.id ] = member.info;
  222. }
  223. });
  224. $module
  225. .data('users', users)
  226. .data('user', users[members.me.id] )
  227. .removeClass(className.loading)
  228. ;
  229. $userList
  230. .html(html)
  231. ;
  232. if( $.fn.preview !== undefined ) {
  233. $userList
  234. .children()
  235. .preview({
  236. type: 'user',
  237. placement: 'left'
  238. })
  239. ;
  240. }
  241. module.user.updateCount();
  242. $.proxy(settings.onJoin, $userList.children())();
  243. },
  244. // shows user list
  245. show: function() {
  246. $log
  247. .animate({
  248. width: (module.width.log - module.width.userList)
  249. }, {
  250. duration : settings.speed,
  251. easing : settings.easing,
  252. complete : module.message.scroll.move
  253. })
  254. ;
  255. },
  256. // hides user list
  257. hide: function() {
  258. $log
  259. .stop()
  260. .animate({
  261. width: (module.width.log)
  262. }, {
  263. duration : settings.speed,
  264. easing : settings.easing,
  265. complete : module.message.scroll.move
  266. })
  267. ;
  268. }
  269. }
  270. },
  271. message: {
  272. // handles scrolling of chat log
  273. scroll: {
  274. test: function() {
  275. height = $log.prop('scrollHeight') - $log.height();
  276. if( Math.abs($log.scrollTop() - height) < settings.scrollArea) {
  277. module.message.scroll.move();
  278. }
  279. },
  280. move: function() {
  281. height = $log.prop('scrollHeight') - $log.height();
  282. $log
  283. .scrollTop(height)
  284. ;
  285. }
  286. },
  287. // sends chat message
  288. send: function(message) {
  289. if( !module.utils.emptyString(message) ) {
  290. $.api({
  291. url : settings.endpoint.message,
  292. method : 'POST',
  293. data : {
  294. 'chat_message': {
  295. content : message,
  296. timestamp : new Date().getTime()
  297. }
  298. }
  299. });
  300. }
  301. },
  302. // receives chat response and processes
  303. receive: function(response) {
  304. message = response.data;
  305. users = $module.data('users');
  306. loggedInUser = $module.data('user');
  307. if(users[ message.userID] !== undefined) {
  308. // logged in user's messages already pushed instantly
  309. if(loggedInUser === undefined || loggedInUser.id != message.userID) {
  310. message.user = users[ message.userID ];
  311. module.message.display(message);
  312. }
  313. }
  314. },
  315. // displays message in chat log
  316. display: function(message) {
  317. $log
  318. .append( settings.templates.message(message) )
  319. ;
  320. module.message.scroll.test();
  321. $.proxy(settings.onMessage, $log.children().last() )();
  322. }
  323. },
  324. expand: function() {
  325. $module
  326. .addClass(className.expand)
  327. ;
  328. $.proxy(settings.onExpand, $module )();
  329. module.refresh();
  330. },
  331. contract: function() {
  332. $module
  333. .removeClass(className.expand)
  334. ;
  335. $.proxy(settings.onContract, $module )();
  336. module.refresh();
  337. },
  338. event: {
  339. input: {
  340. keydown: function(event) {
  341. if(event.which == 13) {
  342. $messageButton
  343. .addClass(className.down)
  344. ;
  345. }
  346. },
  347. keyup: function(event) {
  348. if(event.which == 13) {
  349. $messageButton
  350. .removeClass(className.down)
  351. ;
  352. module.event.submit();
  353. }
  354. }
  355. },
  356. // handles message form submit
  357. submit: function() {
  358. var
  359. message = $messageInput.val(),
  360. loggedInUser = $module.data('user')
  361. ;
  362. if(loggedInUser !== undefined && !module.utils.emptyString(message)) {
  363. module.message.send(message);
  364. // display immediately
  365. module.message.display({
  366. user: loggedInUser,
  367. text: message
  368. });
  369. module.message.scroll.move();
  370. $messageInput
  371. .val('')
  372. ;
  373. }
  374. },
  375. // handles button click on expand button
  376. toggleExpand: function() {
  377. if( !$module.hasClass(className.expand) ) {
  378. $expandButton
  379. .addClass(className.active)
  380. ;
  381. module.expand();
  382. }
  383. else {
  384. $expandButton
  385. .removeClass(className.active)
  386. ;
  387. module.contract();
  388. }
  389. },
  390. // handles button click on user list button
  391. toggleUserList: function() {
  392. if( !$log.is(':animated') ) {
  393. if( !$userListButton.hasClass(className.active) ) {
  394. $userListButton
  395. .addClass(className.active)
  396. ;
  397. module.user.list.show();
  398. }
  399. else {
  400. $userListButton
  401. .removeClass('active')
  402. ;
  403. module.user.list.hide();
  404. }
  405. }
  406. }
  407. },
  408. utils: {
  409. emptyString: function(string) {
  410. if(typeof string == 'string') {
  411. return (string.search(/\S/) == -1);
  412. }
  413. return false;
  414. }
  415. },
  416. // standard methods
  417. debug: function(message) {
  418. if(settings.debug) {
  419. console.info(settings.moduleName + ': ' + message);
  420. }
  421. },
  422. error: function(errorMessage) {
  423. console.warn(settings.moduleName + ': ' + errorMessage);
  424. },
  425. invoke: function(methodName, context, methodArguments) {
  426. var
  427. method
  428. ;
  429. methodArguments = methodArguments || Array.prototype.slice.call( arguments, 2 );
  430. if(typeof methodName == 'string' && instance !== undefined) {
  431. methodName = methodName.split('.');
  432. $.each(methodName, function(index, name) {
  433. if( $.isPlainObject( instance[name] ) ) {
  434. instance = instance[name];
  435. return true;
  436. }
  437. else if( $.isFunction( instance[name] ) ) {
  438. method = instance[name];
  439. return true;
  440. }
  441. module.error(settings.errors.method);
  442. return false;
  443. });
  444. }
  445. return ( $.isFunction( method ) )
  446. ? method.apply(context, methodArguments)
  447. : false
  448. ;
  449. }
  450. };
  451. if(instance !== undefined && moduleArguments) {
  452. // simpler than invoke realizing to invoke itself (and losing scope due prototype.call()
  453. if(moduleArguments[0] == 'invoke') {
  454. moduleArguments = Array.prototype.slice.call( moduleArguments, 1 );
  455. }
  456. return module.invoke(moduleArguments[0], this, Array.prototype.slice.call( moduleArguments, 1 ) );
  457. }
  458. // initializing
  459. module.initialize();
  460. })
  461. ;
  462. return this;
  463. };
  464. $.fn.chat.settings = {
  465. moduleName : 'Chat Module',
  466. debug : false,
  467. namespace : 'chat',
  468. onJoin : function(){},
  469. onMessage : function(){},
  470. onExpand : function(){},
  471. onContract : function(){},
  472. customEvents : {},
  473. partingMessages : false,
  474. userCount : true,
  475. randomColor : true,
  476. speed : 300,
  477. easing : 'easeOutQuint',
  478. // pixels from bottom of chat log that should trigger auto scroll to bottom
  479. scrollArea : 9999,
  480. endpoint : {
  481. message : false,
  482. authentication : false
  483. },
  484. errors: {
  485. method : 'The method you called is not defined',
  486. endpoint : 'Please define a message and authentication endpoint.',
  487. key : 'You must specify a pusher key and channel.',
  488. pusher : 'You must include the Pusher library.'
  489. },
  490. className : {
  491. expand : 'expand',
  492. active : 'active',
  493. hover : 'hover',
  494. down : 'down',
  495. loading : 'loading'
  496. },
  497. selector : {
  498. userCount : '.actions .message',
  499. userListButton : '.actions .button.user-list',
  500. expandButton : '.actions .button.expand',
  501. room : '.room',
  502. userList : '.room .user-list',
  503. log : '.room .log',
  504. message : '.room .log .message',
  505. author : '.room log .message .author',
  506. messageInput : '.talk input',
  507. messageButton : '.talk .send.button'
  508. },
  509. templates: {
  510. userCount: function(number) {
  511. return number + ' users in chat';
  512. },
  513. color: function(userID) {
  514. var
  515. colors = [
  516. '#000000',
  517. '#333333',
  518. '#666666',
  519. '#999999',
  520. '#CC9999',
  521. '#CC6666',
  522. '#CC3333',
  523. '#993333',
  524. '#663333',
  525. '#CC6633',
  526. '#CC9966',
  527. '#CC9933',
  528. '#999966',
  529. '#CCCC66',
  530. '#99CC66',
  531. '#669933',
  532. '#669966',
  533. '#33A3CC',
  534. '#336633',
  535. '#33CCCC',
  536. '#339999',
  537. '#336666',
  538. '#336699',
  539. '#6666CC',
  540. '#9966CC',
  541. '#333399',
  542. '#663366',
  543. '#996699',
  544. '#993366',
  545. '#CC6699'
  546. ]
  547. ;
  548. return colors[ Math.floor( Math.random() * colors.length) ];
  549. },
  550. message: function(message) {
  551. var
  552. html = ''
  553. ;
  554. if(message.user.isAdmin) {
  555. message.user.color = '#55356A';
  556. html += '<div class="admin message">';
  557. html += '<span class="quirky ui flag team"></span>';
  558. }
  559. /*
  560. else if(message.user.isPro) {
  561. html += '<div class="indent message">';
  562. html += '<span class="quirky ui flag pro"></span>';
  563. }
  564. */
  565. else {
  566. html += '<div class="message">';
  567. }
  568. html += '<p>';
  569. if(message.user.color !== undefined) {
  570. html += '<span class="author" style="color: ' + message.user.color + ';">' + message.user.name + '</span>: ';
  571. }
  572. else {
  573. html += '<span class="author">' + message.user.name + '</span>: ';
  574. }
  575. html += ''
  576. + message.text
  577. + ' </p>'
  578. + '</div>'
  579. ;
  580. return html;
  581. },
  582. joined: function(member) {
  583. return (typeof member.name !== undefined)
  584. ? '<div class="status">' + member.name + ' has joined the chat.</div>'
  585. : false
  586. ;
  587. },
  588. left: function(member) {
  589. return (typeof member.name !== undefined)
  590. ? '<div class="status">' + member.name + ' has left the chat.</div>'
  591. : false
  592. ;
  593. },
  594. userList: function(member) {
  595. var
  596. html = ''
  597. ;
  598. if(member.isAdmin) {
  599. member.color = '#55356A';
  600. }
  601. html += ''
  602. + '<div class="user" data-id="' + member.id + '">'
  603. + ' <div class="image">'
  604. + ' <img src="' + member.avatarURL + '">'
  605. + ' </div>'
  606. ;
  607. if(member.color !== undefined) {
  608. html += ' <p><a href="/users/' + member.id + '" target="_blank" style="color: ' + member.color + ';">' + member.name + '</a></p>';
  609. }
  610. else {
  611. html += ' <p><a href="/users/' + member.id + '" target="_blank">' + member.name + '</a></p>';
  612. }
  613. html += '</div>';
  614. return html;
  615. }
  616. }
  617. };
  618. })( jQuery, window , document );