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