b104c7c002b47a805fd840d0b1985538fdefd7e4
[KiwiIRC.git] / client / assets / dev / view.js
1 /*jslint white:true, regexp: true, nomen: true, devel: true, undef: true, browser: true, continue: true, sloppy: true, forin: true, newcap: true, plusplus: true, maxerr: 50, indent: 4 */
2 /*global kiwi */
3
4 _kiwi.view.MemberList = Backbone.View.extend({
5 tagName: "ul",
6 events: {
7 "click .nick": "nickClick"
8 },
9 initialize: function (options) {
10 this.model.bind('all', this.render, this);
11 $(this.el).appendTo('#memberlists');
12 },
13 render: function () {
14 var $this = $(this.el);
15 $this.empty();
16 this.model.forEach(function (member) {
17 var prefix_css_class = (member.get('modes') || []).join(' ');
18 $('<li class="mode ' + prefix_css_class + '"><a class="nick"><span class="prefix">' + member.get("prefix") + '</span>' + member.get("nick") + '</a></li>')
19 .appendTo($this)
20 .data('member', member);
21 });
22 },
23 nickClick: function (event) {
24 var $target = $(event.currentTarget).parent('li'),
25 member = $target.data('member'),
26 userbox;
27
28 event.stopPropagation();
29
30 // If the userbox already exists here, hide it
31 if ($target.find('.userbox').length > 0) {
32 $('.userbox', this.$el).remove();
33 return;
34 }
35
36 userbox = new _kiwi.view.UserBox();
37 userbox.member = member;
38 userbox.channel = this.model.channel;
39
40 if (!this.model.getByNick(_kiwi.gateway.get('nick')).get('is_op')) {
41 userbox.$el.children('.if_op').remove();
42 }
43
44 var menu = new _kiwi.view.MenuBox(member.get('nick') || 'User');
45 menu.addItem('userbox', userbox.$el);
46 menu.show();
47
48 // Position the userbox + menubox
49 (function() {
50 var t = event.pageY,
51 m_bottom = t + menu.$el.outerHeight(), // Where the bottom of menu will be
52 memberlist_bottom = this.$el.parent().offset().top + this.$el.parent().outerHeight();
53
54 // If the bottom of the userbox is going to be too low.. raise it
55 if (m_bottom > memberlist_bottom){
56 t = memberlist_bottom - menu.$el.outerHeight();
57 }
58
59 // Set the new positon
60 menu.$el.offset({
61 left: _kiwi.app.view.$el.width() - menu.$el.outerWidth() - 20,
62 top: t
63 });
64 }).call(this);
65 },
66 show: function () {
67 $('#memberlists').children().removeClass('active');
68 $(this.el).addClass('active');
69 }
70 });
71
72
73
74 _kiwi.view.UserBox = Backbone.View.extend({
75 events: {
76 'click .query': 'queryClick',
77 'click .info': 'infoClick',
78 'click .slap': 'slapClick',
79 'click .op': 'opClick',
80 'click .deop': 'deopClick',
81 'click .voice': 'voiceClick',
82 'click .devoice': 'devoiceClick',
83 'click .kick': 'kickClick',
84 'click .ban': 'banClick'
85 },
86
87 initialize: function () {
88 this.$el = $($('#tmpl_userbox').html());
89 },
90
91 queryClick: function (event) {
92 var panel = new _kiwi.model.Query({name: this.member.get('nick')});
93 _kiwi.app.panels.add(panel);
94 panel.view.show();
95 },
96
97 infoClick: function (event) {
98 _kiwi.app.controlbox.processInput('/whois ' + this.member.get('nick'));
99 },
100
101 slapClick: function (event) {
102 _kiwi.app.controlbox.processInput('/slap ' + this.member.get('nick'));
103 },
104
105 opClick: function (event) {
106 _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +o ' + this.member.get('nick'));
107 },
108
109 deopClick: function (event) {
110 _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -o ' + this.member.get('nick'));
111 },
112
113 voiceClick: function (event) {
114 _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +v ' + this.member.get('nick'));
115 },
116
117 devoiceClick: function (event) {
118 _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -v ' + this.member.get('nick'));
119 },
120
121 kickClick: function (event) {
122 // TODO: Enable the use of a custom kick message
123 _kiwi.app.controlbox.processInput('/kick ' + this.member.get('nick') + ' Bye!');
124 },
125
126 banClick: function (event) {
127 // TODO: Set ban on host, not just on nick
128 _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +b ' + this.member.get('nick') + '!*');
129 }
130 });
131
132 _kiwi.view.NickChangeBox = Backbone.View.extend({
133 events: {
134 'submit': 'changeNick',
135 'click .cancel': 'close'
136 },
137
138 initialize: function () {
139 this.$el = $($('#tmpl_nickchange').html());
140 },
141
142 render: function () {
143 // Add the UI component and give it focus
144 _kiwi.app.controlbox.$el.prepend(this.$el);
145 this.$el.find('input').focus();
146
147 this.$el.css('bottom', _kiwi.app.controlbox.$el.outerHeight(true));
148 },
149
150 close: function () {
151 this.$el.remove();
152
153 },
154
155 changeNick: function (event) {
156 var that = this;
157 _kiwi.gateway.changeNick(this.$el.find('input').val(), function (err, val) {
158 that.close();
159 });
160 return false;
161 }
162 });
163
164 _kiwi.view.ServerSelect = function () {
165 // Are currently showing all the controlls or just a nick_change box?
166 var state = 'all';
167
168 var model = Backbone.View.extend({
169 events: {
170 'submit form': 'submitForm',
171 'click .show_more': 'showMore',
172 'change .have_pass input': 'showPass'
173 },
174
175 initialize: function () {
176 var that = this;
177
178 this.$el = $($('#tmpl_server_select').html());
179
180 // Remove the 'more' link if the server has disabled server changing
181 if (_kiwi.app.server_settings && _kiwi.app.server_settings.connection) {
182 if (!_kiwi.app.server_settings.connection.allow_change) {
183 this.$el.find('.show_more').remove();
184 this.$el.addClass('single_server');
185 }
186 }
187
188
189 _kiwi.gateway.bind('onconnect', this.networkConnected, this);
190 _kiwi.gateway.bind('connecting', this.networkConnecting, this);
191
192 _kiwi.gateway.bind('onirc_error', function (data) {
193 $('button', this.$el).attr('disabled', null);
194
195 if (data.error == 'nickname_in_use') {
196 this.setStatus('Nickname already taken');
197 this.show('nick_change');
198 }
199
200 if (data.error == 'password_mismatch') {
201 this.setStatus('Incorrect Password');
202 this.show('nick_change');
203 that.$el.find('.password').select();
204 }
205 }, this);
206 },
207
208 submitForm: function (event) {
209 event.preventDefault();
210
211 // Make sure a nick is chosen
212 if (!$('input.nick', this.$el).val().trim()) {
213 this.setStatus('Select a nickname first!');
214 $('input.nick', this.$el).select();
215 return;
216 }
217
218 if (state === 'nick_change') {
219 this.submitNickChange(event);
220 } else {
221 this.submitLogin(event);
222 }
223
224 $('button', this.$el).attr('disabled', 1);
225 return;
226 },
227
228 submitLogin: function (event) {
229 // If submitting is disabled, don't do anything
230 if ($('button', this.$el).attr('disabled')) return;
231
232 var values = {
233 nick: $('input.nick', this.$el).val(),
234 server: $('input.server', this.$el).val(),
235 port: $('input.port', this.$el).val(),
236 ssl: $('input.ssl', this.$el).prop('checked'),
237 password: $('input.password', this.$el).val(),
238 channel: $('input.channel', this.$el).val(),
239 channel_key: $('input.channel_key', this.$el).val()
240 };
241
242 this.trigger('server_connect', values);
243 },
244
245 submitNickChange: function (event) {
246 _kiwi.gateway.changeNick($('input.nick', this.$el).val());
247 this.networkConnecting();
248 },
249
250 showPass: function (event) {
251 if (this.$el.find('tr.have_pass input').is(':checked')) {
252 this.$el.find('tr.pass').show().find('input').focus();
253 } else {
254 this.$el.find('tr.pass').hide().find('input').val('');
255 }
256 },
257
258 showMore: function (event) {
259 $('.more', this.$el).slideDown('fast');
260 $('input.server', this.$el).select();
261 },
262
263 populateFields: function (defaults) {
264 var nick, server, port, channel, channel_key, ssl, password;
265
266 defaults = defaults || {};
267
268 nick = defaults.nick || '';
269 server = defaults.server || '';
270 port = defaults.port || 6667;
271 ssl = defaults.ssl || 0;
272 password = defaults.password || '';
273 channel = defaults.channel || '';
274 channel_key = defaults.channel_key || '';
275
276 $('input.nick', this.$el).val(nick);
277 $('input.server', this.$el).val(server);
278 $('input.port', this.$el).val(port);
279 $('input.ssl', this.$el).prop('checked', ssl);
280 $('input.password', this.$el).val(password);
281 $('input.channel', this.$el).val(channel);
282 $('input.channel_key', this.$el).val(channel_key);
283 },
284
285 hide: function () {
286 this.$el.slideUp();
287 },
288
289 show: function (new_state) {
290 new_state = new_state || 'all';
291
292 this.$el.show();
293
294 if (new_state === 'all') {
295 $('.show_more', this.$el).show();
296
297 } else if (new_state === 'more') {
298 $('.more', this.$el).slideDown('fast');
299
300 } else if (new_state === 'nick_change') {
301 $('.more', this.$el).hide();
302 $('.show_more', this.$el).hide();
303 $('input.nick', this.$el).select();
304 }
305
306 state = new_state;
307 },
308
309 setStatus: function (text, class_name) {
310 $('.status', this.$el)
311 .text(text)
312 .attr('class', 'status')
313 .addClass(class_name||'')
314 .show();
315 },
316 clearStatus: function () {
317 $('.status', this.$el).hide();
318 },
319
320 networkConnected: function (event) {
321 this.setStatus('Connected :)', 'ok');
322 $('form', this.$el).hide();
323 },
324
325 networkConnecting: function (event) {
326 this.setStatus('Connecting..', 'ok');
327 },
328
329 showError: function (event) {
330 this.setStatus('Error connecting', 'error');
331 $('button', this.$el).attr('disabled', null);
332 this.show();
333 }
334 });
335
336
337 return new model(arguments);
338 };
339
340
341 _kiwi.view.Panel = Backbone.View.extend({
342 tagName: "div",
343 className: "messages",
344 events: {
345 "click .chan": "chanClick",
346 'click .media .open': 'mediaClick',
347 'mouseenter .msg .nick': 'msgEnter',
348 'mouseleave .msg .nick': 'msgLeave'
349 },
350
351 initialize: function (options) {
352 this.initializePanel(options);
353 },
354
355 initializePanel: function (options) {
356 this.$el.css('display', 'none');
357 options = options || {};
358
359 // Containing element for this panel
360 if (options.container) {
361 this.$container = $(options.container);
362 } else {
363 this.$container = $('#panels .container1');
364 }
365
366 this.$el.appendTo(this.$container);
367
368 this.alert_level = 0;
369
370 this.model.bind('msg', this.newMsg, this);
371 this.msg_count = 0;
372
373 this.model.set({"view": this}, {"silent": true});
374 },
375
376 render: function () {
377 var that = this;
378
379 this.$el.empty();
380 _.each(this.model.get('scrollback'), function (msg) {
381 that.newMsg(msg);
382 });
383 },
384
385 newMsg: function (msg) {
386 var re, line_msg, $this = this.$el,
387 nick_colour_hex, nick_hex, is_highlight, msg_css_classes = '';
388
389 // Nick highlight detecting
390 if ((new RegExp('\\b' + _kiwi.gateway.get('nick') + '\\b', 'i')).test(msg.msg)) {
391 is_highlight = true;
392 msg_css_classes += ' highlight';
393 }
394
395 // Escape any HTML that may be in here
396 msg.msg = $('<div />').text(msg.msg).html();
397
398 // Make the channels clickable
399 re = new RegExp('(?:^|\\s)([' + _kiwi.gateway.get('channel_prefix') + '][^ ,.\\007]+)', 'g');
400 msg.msg = msg.msg.replace(re, function (match) {
401 return '<a class="chan" data-channel="' + match.trim() + '">' + match + '</a>';
402 });
403
404
405 // Parse any links found
406 msg.msg = msg.msg.replace(/(([A-Za-z0-9\-]+\:\/\/)|(www\.))([\w.\-]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w#!:.?$'()[\]*,;~+=&%@!\-\/]*)?/gi, function (url) {
407 var nice = url,
408 extra_html = '';
409
410 // Add the http if no protoocol was found
411 if (url.match(/^www\./)) {
412 url = 'http://' + url;
413 }
414
415 // Shorten the displayed URL if it's going to be too long
416 if (nice.length > 100) {
417 nice = nice.substr(0, 100) + '...';
418 }
419
420 // Get any media HTML if supported
421 extra_html = _kiwi.view.MediaMessage.buildHtml(url);
422
423 // Make the link clickable
424 return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url + '">' + nice + '</a> ' + extra_html;
425 });
426
427
428 // Convert IRC formatting into HTML formatting
429 msg.msg = formatIRCMsg(msg.msg);
430
431
432 // Add some colours to the nick (Method based on IRSSIs nickcolor.pl)
433 nick_colour_hex = (function (nick) {
434 var nick_int = 0, rgb;
435
436 _.map(nick.split(''), function (i) { nick_int += i.charCodeAt(0); });
437 rgb = hsl2rgb(nick_int % 255, 70, 35);
438 rgb = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);
439
440 return '#' + rgb.toString(16);
441 })(msg.nick);
442
443 msg.nick_style = 'color:' + nick_colour_hex + ';';
444
445 // Generate a hex string from the nick to be used as a CSS class name
446 nick_hex = msg.nick_css_class = '';
447 if (msg.nick) {
448 _.map(msg.nick.split(''), function (char) {
449 nick_hex += char.charCodeAt(0).toString(16);
450 });
451 msg_css_classes += ' nick_' + nick_hex;
452 }
453
454 // Build up and add the line
455 msg.msg_css_classes = msg_css_classes;
456 line_msg = '<div class="msg <%= type %> <%= msg_css_classes %>"><div class="time"><%- time %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>';
457 $this.append(_.template(line_msg, msg));
458
459 // Activity/alerts based on the type of new message
460 if (msg.type.match(/^action /)) {
461 this.alert('action');
462
463 } else if (is_highlight) {
464 _kiwi.app.view.alertWindow('* People are talking!');
465 _kiwi.app.view.playSound('highlight');
466 this.alert('highlight');
467
468 } else {
469 // If this is the active panel, send an alert out
470 if (this.model.isActive()) {
471 _kiwi.app.view.alertWindow('* People are talking!');
472 }
473 this.alert('activity');
474 }
475
476 if (this.model.isQuery() && !this.model.isActive()) {
477 _kiwi.app.view.alertWindow('* People are talking!');
478 _kiwi.app.view.playSound('highlight');
479 }
480
481 // Update the activity counters
482 (function () {
483 // Only inrement the counters if we're not the active panel
484 if (this.model.isActive()) return;
485
486 var $act = this.model.tab.find('.activity');
487 $act.text((parseInt($act.text(), 10) || 0) + 1);
488 if ($act.text() === '0') {
489 $act.addClass('zero');
490 } else {
491 $act.removeClass('zero');
492 }
493 }).apply(this);
494
495 this.scrollToBottom();
496
497 // Make sure our DOM isn't getting too large (Acts as scrollback)
498 this.msg_count++;
499 if (this.msg_count > (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250)) {
500 $('.msg:first', this.$el).remove();
501 this.msg_count--;
502 }
503 },
504 chanClick: function (event) {
505 if (event.target) {
506 _kiwi.gateway.join($(event.target).data('channel'));
507 } else {
508 // IE...
509 _kiwi.gateway.join($(event.srcElement).data('channel'));
510 }
511 },
512
513 mediaClick: function (event) {
514 var $media = $(event.target).parents('.media');
515 var media_message;
516
517 if ($media.data('media')) {
518 media_message = $media.data('media');
519 } else {
520 media_message = new _kiwi.view.MediaMessage({el: $media[0]});
521 $media.data('media', media_message);
522 }
523
524 $media.data('media', media_message);
525
526 media_message.open();
527 },
528
529 // Cursor hovers over a message
530 msgEnter: function (event) {
531 var nick_class;
532
533 // Find a valid class that this element has
534 _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
535 if (css_class.match(/^nick_[a-z0-9]+/i)) {
536 nick_class = css_class;
537 }
538 });
539
540 // If no class was found..
541 if (!nick_class) return;
542
543 $('.'+nick_class).addClass('global_nick_highlight');
544 },
545
546 // Cursor leaves message
547 msgLeave: function (event) {
548 var nick_class;
549
550 // Find a valid class that this element has
551 _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
552 if (css_class.match(/^nick_[a-z0-9]+/i)) {
553 nick_class = css_class;
554 }
555 });
556
557 // If no class was found..
558 if (!nick_class) return;
559
560 $('.'+nick_class).removeClass('global_nick_highlight');
561 },
562
563 show: function () {
564 var $this = this.$el;
565
566 // Hide all other panels and show this one
567 this.$container.children().css('display', 'none');
568 $this.css('display', 'block');
569
570 // Show this panels memberlist
571 var members = this.model.get("members");
572 if (members) {
573 $('#memberlists').removeClass('disabled');
574 members.view.show();
575 } else {
576 // Memberlist not found for this panel, hide any active ones
577 $('#memberlists').addClass('disabled').children().removeClass('active');
578 }
579
580 _kiwi.app.view.doLayout();
581
582 // Remove any alerts and activity counters for this panel
583 this.alert('none');
584 this.model.tab.find('.activity').text('0').addClass('zero');
585
586 this.trigger('active', this.model);
587 _kiwi.app.panels.trigger('active', this.model, _kiwi.app.panels.active);
588
589 this.scrollToBottom(true);
590 },
591
592
593 alert: function (level) {
594 // No need to highlight if this si the active panel
595 if (this.model == _kiwi.app.panels.active) return;
596
597 var types, type_idx;
598 types = ['none', 'action', 'activity', 'highlight'];
599
600 // Default alert level
601 level = level || 'none';
602
603 // If this alert level does not exist, assume clearing current level
604 type_idx = _.indexOf(types, level);
605 if (!type_idx) {
606 level = 'none';
607 type_idx = 0;
608 }
609
610 // Only 'upgrade' the alert. Never down (unless clearing)
611 if (type_idx !== 0 && type_idx <= this.alert_level) {
612 return;
613 }
614
615 // Clear any existing levels
616 this.model.tab.removeClass(function (i, css) {
617 return (css.match(/\balert_\S+/g) || []).join(' ');
618 });
619
620 // Add the new level if there is one
621 if (level !== 'none') {
622 this.model.tab.addClass('alert_' + level);
623 }
624
625 this.alert_level = type_idx;
626 },
627
628
629 // Scroll to the bottom of the panel
630 scrollToBottom: function (force_down) {
631 // If this isn't the active panel, don't scroll
632 if (this.model !== _kiwi.app.panels.active) return;
633
634 // Don't scroll down if we're scrolled up the panel a little
635 if (force_down || this.$container.scrollTop() + this.$container.height() > this.$el.outerHeight() - 150) {
636 this.$container[0].scrollTop = this.$container[0].scrollHeight;
637 }
638 }
639 });
640
641 _kiwi.view.Applet = _kiwi.view.Panel.extend({
642 className: 'applet',
643 initialize: function (options) {
644 this.initializePanel(options);
645 }
646 });
647
648 _kiwi.view.Channel = _kiwi.view.Panel.extend({
649 initialize: function (options) {
650 this.initializePanel(options);
651 this.model.bind('change:topic', this.topic, this);
652
653 // Only show the loader if this is a channel (ie. not a query)
654 if (this.model.isChannel()) {
655 this.$el.append('<div class="initial_loader" style="margin:1em;text-align:center;">Joining channel.. <span class="loader"></span></div>');
656 }
657 },
658
659 // Override the existing newMsg() method to remove the joining channel loader
660 newMsg: function () {
661 this.$el.find('.initial_loader').slideUp(function () {
662 $(this).remove();
663 });
664
665 return this.constructor.__super__.newMsg.apply(this, arguments);
666 },
667
668 topic: function (topic) {
669 if (typeof topic !== 'string' || !topic) {
670 topic = this.model.get("topic");
671 }
672
673 this.model.addMsg('', '== Topic for ' + this.model.get('name') + ' is: ' + topic, 'topic');
674
675 // If this is the active channel then update the topic bar
676 if (_kiwi.app.panels.active === this) {
677 _kiwi.app.topicbar.setCurrentTopic(this.model.get("topic"));
678 }
679 }
680 });
681
682 // Model for this = _kiwi.model.PanelList
683 _kiwi.view.Tabs = Backbone.View.extend({
684 tagName: 'ul',
685
686 events: {
687 'click li': 'tabClick',
688 'click li .part': 'partClick'
689 },
690
691 initialize: function () {
692 this.model.on("add", this.panelAdded, this);
693 this.model.on("remove", this.panelRemoved, this);
694 this.model.on("reset", this.render, this);
695
696 this.model.on('active', this.panelActive, this);
697
698 this.model.network.on('change:name', function (network, new_val) {
699 $('span', this.model.server.tab).text(new_val);
700 }, this);
701
702 this.$tab_container = $('#kiwi .panellist.channels');
703 this.$tab_container.append(this.$el);
704 },
705
706 render: function () {
707 var that = this;
708
709 this.$el.empty();
710
711 // Add the server tab first
712 this.model.server.tab
713 .data('panel', this.model.server)
714 .data('connection_id', this.model.network.get('connection_id'))
715 .appendTo(this.$el);
716
717 // Go through each panel adding its tab
718 this.model.forEach(function (panel) {
719 // If this is the server panel, ignore as it's already added
720 if (panel == that.model.server) return;
721
722 panel.tab
723 .data('panel', panel)
724 .appendTo(that.$el);
725 //.appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg);
726 });
727
728 _kiwi.app.view.doLayout();
729 },
730
731 updateTabTitle: function (panel, new_title) {
732 $('span', panel.tab).text(new_title);
733 },
734
735 panelAdded: function (panel) {
736 // Add a tab to the panel
737 panel.tab = $('<li><span>' + (panel.get('title') || panel.get('name')) + '</span><div class="activity"></div></li>');
738
739 if (panel.isServer()) {
740 panel.tab.addClass('server');
741 panel.tab.addClass('icon-nonexistant');
742 }
743
744 panel.tab.data('panel', panel)
745 .data('connection_id', this.model.network.get('connection_id'))
746 .appendTo(this.$el);
747 //.appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg);
748
749 panel.bind('change:title', this.updateTabTitle);
750 _kiwi.app.view.doLayout();
751 },
752 panelRemoved: function (panel) {
753 panel.tab.remove();
754 delete panel.tab;
755
756 _kiwi.app.view.doLayout();
757 },
758
759 panelActive: function (panel, previously_active_panel) {
760 // Remove any existing tabs or part images
761 $('.part', this.$el).remove();
762 this.$tab_container.find('.active').removeClass('active');
763
764 panel.tab.addClass('active');
765
766 // Only show the part image on non-server tabs
767 if (!panel.isServer()) {
768 panel.tab.append('<span class="part icon-nonexistant"></span>');
769 }
770 },
771
772 tabClick: function (e) {
773 var tab = $(e.currentTarget);
774
775 var panel = tab.data('panel');
776 if (!panel) {
777 // A panel wasn't found for this tab... wadda fuck
778 return;
779 }
780
781 panel.view.show();
782 },
783
784 partClick: function (e) {
785 var tab = $(e.currentTarget).parent();
786 var panel = this.model.getByCid(tab.data('panel'));
787
788 // Only need to part if it's a channel
789 // If the nicklist is empty, we haven't joined the channel as yet
790 if (panel.isChannel() && panel.get('members').models.length > 0) {
791 this.model.network.gateway.part(panel.get('name'));
792 } else {
793 panel.close();
794 }
795 },
796
797 next: function () {
798 var next = this.$tab_container.find('.active').next();
799 if (!next.length) next = $('li:first', this.$tab_container);
800
801 next.click();
802 },
803 prev: function () {
804 var prev = this.$tab_container.find('.active').prev();
805 if (!prev.length) prev = $('li:last', this.$tab_container);
806
807 prev.click();
808 }
809 });
810
811
812
813 _kiwi.view.TopicBar = Backbone.View.extend({
814 events: {
815 'keydown div': 'process'
816 },
817
818 initialize: function () {
819 _kiwi.app.panels.bind('active', function (active_panel) {
820 // If it's a channel topic, update and make editable
821 if (active_panel.isChannel()) {
822 this.setCurrentTopic(active_panel.get('topic') || '');
823 this.$el.find('div').attr('contentEditable', true);
824
825 } else {
826 // Not a channel topic.. clear and make uneditable
827 this.$el.find('div').attr('contentEditable', false)
828 .text('');
829 }
830 }, this);
831 },
832
833 process: function (ev) {
834 var inp = $(ev.currentTarget),
835 inp_val = inp.text();
836
837 // Only allow topic editing if this is a channel panel
838 if (!_kiwi.app.panels.active.isChannel()) {
839 return false;
840 }
841
842 // If hit return key, update the current topic
843 if (ev.keyCode === 13) {
844 _kiwi.gateway.topic(_kiwi.app.panels.active.get('name'), inp_val);
845 return false;
846 }
847 },
848
849 setCurrentTopic: function (new_topic) {
850 new_topic = new_topic || '';
851
852 // We only want a plain text version
853 $('div', this.$el).html(formatIRCMsg(_.escape(new_topic)));
854 }
855 });
856
857
858
859 _kiwi.view.ControlBox = Backbone.View.extend({
860 events: {
861 'keydown .inp': 'process',
862 'click .nick': 'showNickChange'
863 },
864
865 initialize: function () {
866 var that = this;
867
868 this.buffer = []; // Stores previously run commands
869 this.buffer_pos = 0; // The current position in the buffer
870
871 this.preprocessor = new InputPreProcessor();
872 this.preprocessor.recursive_depth = 5;
873
874 // Hold tab autocomplete data
875 this.tabcomplete = {active: false, data: [], prefix: ''};
876
877 _kiwi.gateway.bind('change:nick', function () {
878 $('.nick', that.$el).text(this.get('nick'));
879 });
880
881 _kiwi.app.connections.on('active', function(panel, connection) {
882 $('.nick', that.$el).text(connection.get('nick'));
883 });
884 },
885
886 showNickChange: function (ev) {
887 (new _kiwi.view.NickChangeBox()).render();
888 },
889
890 process: function (ev) {
891 var that = this,
892 inp = $(ev.currentTarget),
893 inp_val = inp.val(),
894 meta;
895
896 if (navigator.appVersion.indexOf("Mac") !== -1) {
897 meta = ev.metaKey;
898 } else {
899 meta = ev.altKey;
900 }
901
902 // If not a tab key, reset the tabcomplete data
903 if (this.tabcomplete.active && ev.keyCode !== 9) {
904 this.tabcomplete.active = false;
905 this.tabcomplete.data = [];
906 this.tabcomplete.prefix = '';
907 }
908
909 switch (true) {
910 case (ev.keyCode === 13): // return
911 inp_val = inp_val.trim();
912
913 if (inp_val) {
914 $.each(inp_val.split('\n'), function (idx, line) {
915 that.processInput(line);
916 });
917
918 this.buffer.push(inp_val);
919 this.buffer_pos = this.buffer.length;
920 }
921
922 inp.val('');
923 return false;
924
925 break;
926
927 case (ev.keyCode === 38): // up
928 if (this.buffer_pos > 0) {
929 this.buffer_pos--;
930 inp.val(this.buffer[this.buffer_pos]);
931 }
932 break;
933
934 case (ev.keyCode === 40): // down
935 if (this.buffer_pos < this.buffer.length) {
936 this.buffer_pos++;
937 inp.val(this.buffer[this.buffer_pos]);
938 }
939 break;
940
941 case (ev.keyCode === 219 && meta): // [ + meta
942 _kiwi.app.panels.view.prev();
943 return false;
944
945 case (ev.keyCode === 221 && meta): // ] + meta
946 _kiwi.app.panels.view.next();
947 return false;
948
949 case (ev.keyCode === 9): // tab
950 this.tabcomplete.active = true;
951 if (_.isEqual(this.tabcomplete.data, [])) {
952 // Get possible autocompletions
953 var ac_data = [];
954 $.each(_kiwi.app.panels.active.get('members').models, function (i, member) {
955 if (!member) return;
956 ac_data.push(member.get('nick'));
957 });
958 ac_data = _.sortBy(ac_data, function (nick) {
959 return nick;
960 });
961 this.tabcomplete.data = ac_data;
962 }
963
964 if (inp_val[inp[0].selectionStart - 1] === ' ') {
965 return false;
966 }
967
968 (function () {
969 var tokens, // Words before the cursor position
970 val, // New value being built up
971 p1, // Position in the value just before the nick
972 newnick, // New nick to be displayed (cycles through)
973 range, // TextRange for setting new text cursor position
974 nick, // Current nick in the value
975 trailing = ': '; // Text to be inserted after a tabbed nick
976
977 tokens = inp_val.substring(0, inp[0].selectionStart).split(' ');
978 if (tokens[tokens.length-1] == ':')
979 tokens.pop();
980
981 nick = tokens[tokens.length - 1];
982
983 if (this.tabcomplete.prefix === '') {
984 this.tabcomplete.prefix = nick;
985 }
986
987 this.tabcomplete.data = _.select(this.tabcomplete.data, function (n) {
988 return (n.toLowerCase().indexOf(that.tabcomplete.prefix.toLowerCase()) === 0);
989 });
990
991 if (this.tabcomplete.data.length > 0) {
992 // Get the current value before cursor position
993 p1 = inp[0].selectionStart - (nick.length);
994 val = inp_val.substr(0, p1);
995
996 // Include the current selected nick
997 newnick = this.tabcomplete.data.shift();
998 this.tabcomplete.data.push(newnick);
999 val += newnick;
1000
1001 if (inp_val.substr(inp[0].selectionStart, 2) !== trailing)
1002 val += trailing;
1003
1004 // Now include the rest of the current value
1005 val += inp_val.substr(inp[0].selectionStart);
1006
1007 inp.val(val);
1008
1009 // Move the cursor position to the end of the nick
1010 if (inp[0].setSelectionRange) {
1011 inp[0].setSelectionRange(p1 + newnick.length + trailing.length, p1 + newnick.length + trailing.length);
1012 } else if (inp[0].createTextRange) { // not sure if this bit is actually needed....
1013 range = inp[0].createTextRange();
1014 range.collapse(true);
1015 range.moveEnd('character', p1 + newnick.length + trailing.length);
1016 range.moveStart('character', p1 + newnick.length + trailing.length);
1017 range.select();
1018 }
1019 }
1020 }).apply(this);
1021 return false;
1022 }
1023 },
1024
1025
1026 processInput: function (command_raw) {
1027 var command, params,
1028 pre_processed;
1029
1030 // The default command
1031 if (command_raw[0] !== '/' || command_raw.substr(0, 2) === '//') {
1032 // Remove any slash escaping at the start (ie. //)
1033 command_raw = command_raw.replace(/^\/\//, '/');
1034
1035 // Prepend the default command
1036 command_raw = '/msg ' + _kiwi.app.panels.active.get('name') + ' ' + command_raw;
1037 }
1038
1039 // Process the raw command for any aliases
1040 this.preprocessor.vars.server = _kiwi.gateway.get('name');
1041 this.preprocessor.vars.channel = _kiwi.app.panels.active.get('name');
1042 this.preprocessor.vars.destination = this.preprocessor.vars.channel;
1043 command_raw = this.preprocessor.process(command_raw);
1044
1045 // Extract the command and parameters
1046 params = command_raw.split(' ');
1047 if (params[0][0] === '/') {
1048 command = params[0].substr(1).toLowerCase();
1049 params = params.splice(1, params.length - 1);
1050 } else {
1051 // Default command
1052 command = 'msg';
1053 params.unshift(_kiwi.app.panels.active.get('name'));
1054 }
1055
1056 // Trigger the command events
1057 this.trigger('command', {command: command, params: params});
1058 this.trigger('command:' + command, {command: command, params: params});
1059
1060 // If we didn't have any listeners for this event, fire a special case
1061 // TODO: This feels dirty. Should this really be done..?
1062 if (!this._callbacks['command:' + command]) {
1063 this.trigger('unknown_command', {command: command, params: params});
1064 }
1065 },
1066
1067
1068 addPluginIcon: function ($icon) {
1069 var $tool = $('<div class="tool"></div>').append($icon);
1070 this.$el.find('.input_tools').append($tool);
1071 _kiwi.app.view.doLayout();
1072 }
1073 });
1074
1075
1076
1077
1078 _kiwi.view.StatusMessage = Backbone.View.extend({
1079 initialize: function () {
1080 this.$el.hide();
1081
1082 // Timer for hiding the message after X seconds
1083 this.tmr = null;
1084 },
1085
1086 text: function (text, opt) {
1087 // Defaults
1088 opt = opt || {};
1089 opt.type = opt.type || '';
1090 opt.timeout = opt.timeout || 5000;
1091
1092 this.$el.text(text).attr('class', opt.type);
1093 this.$el.slideDown($.proxy(_kiwi.app.view.doLayout, this));
1094
1095 if (opt.timeout) this.doTimeout(opt.timeout);
1096 },
1097
1098 html: function (html, opt) {
1099 // Defaults
1100 opt = opt || {};
1101 opt.type = opt.type || '';
1102 opt.timeout = opt.timeout || 5000;
1103
1104 this.$el.html(text).attr('class', opt.type);
1105 this.$el.slideDown(_kiwi.app.view.doLayout);
1106
1107 if (opt.timeout) this.doTimeout(opt.timeout);
1108 },
1109
1110 hide: function () {
1111 this.$el.slideUp($.proxy(_kiwi.app.view.doLayout, this));
1112 },
1113
1114 doTimeout: function (length) {
1115 if (this.tmr) clearTimeout(this.tmr);
1116 var that = this;
1117 this.tmr = setTimeout(function () { that.hide(); }, length);
1118 }
1119 });
1120
1121
1122
1123
1124 _kiwi.view.ResizeHandler = Backbone.View.extend({
1125 events: {
1126 'mousedown': 'startDrag',
1127 'mouseup': 'stopDrag'
1128 },
1129
1130 initialize: function () {
1131 this.dragging = false;
1132 this.starting_width = {};
1133
1134 $(window).on('mousemove', $.proxy(this.onDrag, this));
1135 },
1136
1137 startDrag: function (event) {
1138 this.dragging = true;
1139 },
1140
1141 stopDrag: function (event) {
1142 this.dragging = false;
1143 },
1144
1145 onDrag: function (event) {
1146 if (!this.dragging) return;
1147
1148 this.$el.css('left', event.clientX - (this.$el.outerWidth(true) / 2));
1149 $('#memberlists').css('width', this.$el.parent().width() - (this.$el.position().left + this.$el.outerWidth()));
1150 _kiwi.app.view.doLayout();
1151 }
1152 });
1153
1154
1155
1156 _kiwi.view.AppToolbar = Backbone.View.extend({
1157 events: {
1158 'click .settings': 'clickSettings'
1159 },
1160
1161 initialize: function () {
1162 },
1163
1164 clickSettings: function (event) {
1165 _kiwi.app.controlbox.processInput('/settings');
1166 }
1167 });
1168
1169
1170
1171 _kiwi.view.Application = Backbone.View.extend({
1172 initialize: function () {
1173 var that = this;
1174
1175 $(window).resize(function() { that.doLayout.apply(that); });
1176 $('#toolbar').resize(function() { that.doLayout.apply(that); });
1177 $('#controlbox').resize(function() { that.doLayout.apply(that); });
1178
1179 // Change the theme when the config is changed
1180 _kiwi.global.settings.on('change:theme', this.updateTheme, this);
1181 this.updateTheme(getQueryVariable('theme'));
1182
1183 _kiwi.global.settings.on('change:channel_list_style', this.setTabLayout, this);
1184 this.setTabLayout(_kiwi.global.settings.get('channel_list_style'));
1185
1186 _kiwi.global.settings.on('change:show_timestamps', this.displayTimestamps, this);
1187 this.displayTimestamps(_kiwi.global.settings.get('show_timestamps'));
1188
1189 this.doLayout();
1190
1191 $(document).keydown(this.setKeyFocus);
1192
1193 // Confirmation require to leave the page
1194 window.onbeforeunload = function () {
1195 if (_kiwi.gateway.isConnected()) {
1196 return 'This will close all KiwiIRC conversations. Are you sure you want to close this window?';
1197 }
1198 };
1199
1200 this.initSound();
1201 },
1202
1203
1204
1205 updateTheme: function (theme_name) {
1206 // If called by the settings callback, get the correct new_value
1207 if (theme_name === _kiwi.global.settings) {
1208 theme_name = arguments[1];
1209 }
1210
1211 // If we have no theme specified, get it from the settings
1212 if (!theme_name) theme_name = _kiwi.global.settings.get('theme');
1213
1214 // Clear any current theme
1215 this.$el.removeClass(function (i, css) {
1216 return (css.match(/\btheme_\S+/g) || []).join(' ');
1217 });
1218
1219 // Apply the new theme
1220 this.$el.addClass('theme_' + (theme_name || 'relaxed'));
1221 },
1222
1223
1224 setTabLayout: function (layout_style) {
1225 // If called by the settings callback, get the correct new_value
1226 if (layout_style === _kiwi.global.settings) {
1227 layout_style = arguments[1];
1228 }
1229
1230 if (layout_style == 'list') {
1231 this.$el.addClass('chanlist_treeview');
1232 } else {
1233 this.$el.removeClass('chanlist_treeview');
1234 }
1235
1236 this.doLayout();
1237 },
1238
1239
1240 displayTimestamps: function (show_timestamps) {
1241 // If called by the settings callback, get the correct new_value
1242 if (show_timestamps === _kiwi.global.settings) {
1243 show_timestamps = arguments[1];
1244 }
1245
1246 if (show_timestamps) {
1247 this.$el.addClass('timestamps');
1248 } else {
1249 this.$el.removeClass('timestamps');
1250 }
1251 },
1252
1253
1254 // Globally shift focus to the command input box on a keypress
1255 setKeyFocus: function (ev) {
1256 // If we're copying text, don't shift focus
1257 if (ev.ctrlKey || ev.altKey || ev.metaKey) {
1258 return;
1259 }
1260
1261 // If we're typing into an input box somewhere, ignore
1262 if ((ev.target.tagName.toLowerCase() === 'input') || (ev.target.tagName.toLowerCase() === 'textarea') || $(ev.target).attr('contenteditable')) {
1263 return;
1264 }
1265
1266 $('#controlbox .inp').focus();
1267 },
1268
1269
1270 doLayout: function () {
1271 var el_kiwi = this.$el;
1272 var el_panels = $('#kiwi #panels');
1273 var el_memberlists = $('#kiwi #memberlists');
1274 var el_toolbar = $('#kiwi #toolbar');
1275 var el_controlbox = $('#kiwi #controlbox');
1276 var el_resize_handle = $('#kiwi #memberlists_resize_handle');
1277
1278 var css_heights = {
1279 top: el_toolbar.outerHeight(true),
1280 bottom: el_controlbox.outerHeight(true)
1281 };
1282
1283
1284 // If any elements are not visible, full size the panals instead
1285 if (!el_toolbar.is(':visible')) {
1286 css_heights.top = 0;
1287 }
1288
1289 if (!el_controlbox.is(':visible')) {
1290 css_heights.bottom = 0;
1291 }
1292
1293 // Apply the CSS sizes
1294 el_panels.css(css_heights);
1295 el_memberlists.css(css_heights);
1296 el_resize_handle.css(css_heights);
1297
1298 // If we have channel tabs on the side, adjust the height
1299 if (el_kiwi.hasClass('chanlist_treeview')) {
1300 $('#tabs', el_kiwi).css(css_heights);
1301 }
1302
1303 // Determine if we have a narrow window (mobile/tablet/or even small desktop window)
1304 if (el_kiwi.outerWidth() < 400) {
1305 el_kiwi.addClass('narrow');
1306 } else {
1307 el_kiwi.removeClass('narrow');
1308 }
1309
1310 // Set the panels width depending on the memberlist visibility
1311 if (el_memberlists.css('display') != 'none') {
1312 // Panels to the side of the memberlist
1313 el_panels.css('right', el_memberlists.outerWidth(true));
1314 // The resize handle sits overlapping the panels and memberlist
1315 el_resize_handle.css('left', el_memberlists.position().left - (el_resize_handle.outerWidth(true) / 2));
1316 } else {
1317 // Memberlist is hidden so panels to the right edge
1318 el_panels.css('right', 0);
1319 // And move the handle just out of sight to the right
1320 el_resize_handle.css('left', el_panels.outerWidth(true));
1321 }
1322
1323 var input_wrap_width = parseInt($('#kiwi #controlbox .input_tools').outerWidth());
1324 el_controlbox.find('.input_wrap').css('right', input_wrap_width + 7);
1325 },
1326
1327
1328 alertWindow: function (title) {
1329 if (!this.alertWindowTimer) {
1330 this.alertWindowTimer = new (function () {
1331 var that = this;
1332 var tmr;
1333 var has_focus = true;
1334 var state = 0;
1335 var default_title = 'Kiwi IRC';
1336 var title = 'Kiwi IRC';
1337
1338 this.setTitle = function (new_title) {
1339 new_title = new_title || default_title;
1340 window.document.title = new_title;
1341 return new_title;
1342 };
1343
1344 this.start = function (new_title) {
1345 // Don't alert if we already have focus
1346 if (has_focus) return;
1347
1348 title = new_title;
1349 if (tmr) return;
1350 tmr = setInterval(this.update, 1000);
1351 };
1352
1353 this.stop = function () {
1354 // Stop the timer and clear the title
1355 if (tmr) clearInterval(tmr);
1356 tmr = null;
1357 this.setTitle();
1358
1359 // Some browsers don't always update the last title correctly
1360 // Wait a few seconds and then reset
1361 setTimeout(this.reset, 2000);
1362 };
1363
1364 this.reset = function () {
1365 if (tmr) return;
1366 that.setTitle();
1367 };
1368
1369
1370 this.update = function () {
1371 if (state === 0) {
1372 that.setTitle(title);
1373 state = 1;
1374 } else {
1375 that.setTitle();
1376 state = 0;
1377 }
1378 };
1379
1380 $(window).focus(function (event) {
1381 has_focus = true;
1382 that.stop();
1383
1384 // Some browsers don't always update the last title correctly
1385 // Wait a few seconds and then reset
1386 setTimeout(that.reset, 2000);
1387 });
1388
1389 $(window).blur(function (event) {
1390 has_focus = false;
1391 });
1392 })();
1393 }
1394
1395 this.alertWindowTimer.start(title);
1396 },
1397
1398
1399 barsHide: function (instant) {
1400 var that = this;
1401
1402 if (!instant) {
1403 $('#toolbar').slideUp({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
1404 $('#controlbox').slideUp({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
1405 } else {
1406 $('#toolbar').slideUp(0);
1407 $('#controlbox').slideUp(0);
1408 this.doLayout();
1409 }
1410 },
1411
1412 barsShow: function (instant) {
1413 var that = this;
1414
1415 if (!instant) {
1416 $('#toolbar').slideDown({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
1417 $('#controlbox').slideDown({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
1418 } else {
1419 $('#toolbar').slideDown(0);
1420 $('#controlbox').slideDown(0);
1421 this.doLayout();
1422 }
1423 },
1424
1425
1426 initSound: function () {
1427 var that = this,
1428 base_path = this.model.get('base_path');
1429
1430 $script(base_path + '/assets/libs/soundmanager2/soundmanager2-nodebug-jsmin.js', function() {
1431 if (typeof soundManager === 'undefined')
1432 return;
1433
1434 soundManager.setup({
1435 url: base_path + '/assets/libs/soundmanager2/',
1436 flashVersion: 9, // optional: shiny features (default = 8)// optional: ignore Flash where possible, use 100% HTML5 mode
1437 preferFlash: true,
1438
1439 onready: function() {
1440 that.sound_object = soundManager.createSound({
1441 id: 'highlight',
1442 url: base_path + '/assets/sound/highlight.mp3'
1443 });
1444 }
1445 });
1446 });
1447 },
1448
1449
1450 playSound: function (sound_id) {
1451 if (!this.sound_object) return;
1452
1453 if (_kiwi.global.settings.get('mute_sounds'))
1454 return;
1455
1456 soundManager.play(sound_id);
1457 }
1458 });
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468 _kiwi.view.MediaMessage = Backbone.View.extend({
1469 events: {
1470 'click .media_close': 'close'
1471 },
1472
1473 initialize: function () {
1474 // Get the URL from the data
1475 this.url = this.$el.data('url');
1476 },
1477
1478 // Close the media content and remove it from display
1479 close: function () {
1480 var that = this;
1481 this.$content.slideUp('fast', function () {
1482 that.$content.remove();
1483 });
1484 },
1485
1486 // Open the media content within its wrapper
1487 open: function () {
1488 // Create the content div if we haven't already
1489 if (!this.$content) {
1490 this.$content = $('<div class="media_content"><a class="media_close"><i class="icon-chevron-up"></i> Close media</a><br /><div class="content"></div></div>');
1491 this.$content.find('.content').append(this.mediaTypes[this.$el.data('type')].apply(this, []) || 'Not found :(');
1492 }
1493
1494 // Now show the content if not already
1495 if (!this.$content.is(':visible')) {
1496 // Hide it first so the slideDown always plays
1497 this.$content.hide();
1498
1499 // Add the media content and slide it into view
1500 this.$el.append(this.$content);
1501 this.$content.slideDown();
1502 }
1503 },
1504
1505
1506
1507 // Generate the media content for each recognised type
1508 mediaTypes: {
1509 twitter: function () {
1510 var tweet_id = this.$el.data('tweetid');
1511 var that = this;
1512
1513 $.getJSON('https://api.twitter.com/1/statuses/oembed.json?id=' + tweet_id + '&callback=?', function (data) {
1514 that.$content.find('.content').html(data.html);
1515 });
1516
1517 return $('<div>Loading tweet..</div>');
1518 },
1519
1520
1521 image: function () {
1522 return $('<a href="' + this.url + '" target="_blank"><img height="100" src="' + this.url + '" /></a>');
1523 },
1524
1525
1526 reddit: function () {
1527 var that = this;
1528 var matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(this.url);
1529
1530 $.getJSON('http://www.' + matches[0] + '.json?jsonp=?', function (data) {
1531 console.log('Loaded reddit data', data);
1532 var post = data[0].data.children[0].data;
1533 var thumb = '';
1534
1535 // Show a thumbnail if there is one
1536 if (post.thumbnail) {
1537 //post.thumbnail = 'http://www.eurotunnel.com/uploadedImages/commercial/back-steps-icon-arrow.png';
1538
1539 // Hide the thumbnail if an over_18 image
1540 if (post.over_18) {
1541 thumb = '<span class="thumbnail_nsfw" onclick="$(this).find(\'p\').remove(); $(this).find(\'img\').css(\'visibility\', \'visible\');">';
1542 thumb += '<p style="font-size:0.9em;line-height:1.2em;cursor:pointer;">Show<br />NSFW</p>';
1543 thumb += '<img src="' + post.thumbnail + '" class="thumbnail" style="visibility:hidden;" />';
1544 thumb += '</span>';
1545 } else {
1546 thumb = '<img src="' + post.thumbnail + '" class="thumbnail" />';
1547 }
1548 }
1549
1550 // Build the template string up
1551 var tmpl = '<div>' + thumb + '<b><%- title %></b><br />Posted by <%- author %>. &nbsp;&nbsp; ';
1552 tmpl += '<i class="icon-arrow-up"></i> <%- ups %> &nbsp;&nbsp; <i class="icon-arrow-down"></i> <%- downs %><br />';
1553 tmpl += '<%- num_comments %> comments made. <a href="http://www.reddit.com<%- permalink %>">View post</a></div>';
1554
1555 that.$content.find('.content').html(_.template(tmpl, post));
1556 });
1557
1558 return $('<div>Loading Reddit thread..</div>');
1559 }
1560 }
1561
1562 }, {
1563
1564 // Build the closed media HTML from a URL
1565 buildHtml: function (url) {
1566 var html = '', matches;
1567
1568 // Is it an image?
1569 if (url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
1570 html += '<span class="media image" data-type="image" data-url="' + url + '" title="Open Image"><a class="open"><i class="icon-chevron-right"></i></a></span>';
1571 }
1572
1573 // Is it a tweet?
1574 matches = (/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/ig).exec(url);
1575 if (matches) {
1576 html += '<span class="media twitter" data-type="twitter" data-url="' + url + '" data-tweetid="' + matches[2] + '" title="Show tweet information"><a class="open"><i class="icon-chevron-right"></i></a></span>';
1577 }
1578
1579 // Is reddit?
1580 matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(url);
1581 if (matches) {
1582 html += '<span class="media reddit" data-type="reddit" data-url="' + url + '" title="Reddit thread"><a class="open"><i class="icon-chevron-right"></i></a></span>';
1583 }
1584
1585 return html;
1586 }
1587 });
1588
1589
1590
1591 _kiwi.view.MenuBox = Backbone.View.extend({
1592 events: {
1593 'click .ui_menu_foot .close': 'dispose'
1594 },
1595
1596 initialize: function(title) {
1597 var that = this;
1598
1599 this.$el = $('<div class="ui_menu"></div>');
1600
1601 this._title = title || '';
1602 this._items = {};
1603 this._display_footer = true;
1604
1605 this._close_proxy = function(event) {
1606 that.onDocumentClick(event);
1607 };
1608 $(document).on('click', this._close_proxy);
1609 },
1610
1611
1612 render: function() {
1613 var that = this;
1614
1615 this.$el.find('*').remove();
1616
1617 if (this._title) {
1618 $('<div class="ui_menu_title"></div>')
1619 .text(this._title)
1620 .appendTo(this.$el);
1621 }
1622
1623
1624 _.each(this._items, function(item) {
1625 var $item = $('<div class="ui_menu_content hover"></div>')
1626 .append(item);
1627
1628 that.$el.append($item);
1629 });
1630
1631 if (this._display_footer)
1632 this.$el.append('<div class="ui_menu_foot"><a class="close" onclick="">Close <i class="icon-remove"></i></a></div>');
1633 },
1634
1635
1636 onDocumentClick: function(event) {
1637 var $target = $(event.target);
1638
1639 // If this is not itself AND we don't contain this element, dispose $el
1640 if ($target[0] != this.$el[0] && this.$el.has($target).length === 0)
1641 this.dispose();
1642 },
1643
1644
1645 dispose: function() {
1646 _.each(this._items, function(item) {
1647 item.dispose && item.dispose();
1648 item.remove && item.remove();
1649 });
1650
1651 this._items = null;
1652 this.remove();
1653
1654 $(document).off('click', this._close_proxy);
1655 },
1656
1657
1658 addItem: function(item_name, $item) {
1659 $item = $($item);
1660 if ($item.is('a')) $item.addClass('icon-chevron-right');
1661 this._items[item_name] = $item;
1662 },
1663
1664
1665 removeItem: function(item_name) {
1666 delete this._items[item_name];
1667 },
1668
1669
1670 showFooter: function(show) {
1671 this._show_footer = show;
1672 },
1673
1674
1675 show: function() {
1676 this.render();
1677 this.$el.appendTo(_kiwi.app.view.$el);
1678 }
1679 });