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