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