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.

776 lines
22 KiB

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