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.

769 lines
22 KiB

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