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.

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