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.

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