Actually remove node version restriction
[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 // Only show the loader if this is a channel (ie. not a query)
583 if (this.model.isChannel()) {
584 this.$el.append('<div class="initial_loader" style="margin:1em;text-align:center;">Joining channel.. <span class="loader"></span></div>');
585 }
586 },
587
588 // Override the existing newMsg() method to remove the joining channel loader
589 newMsg: function () {
590 this.$el.find('.initial_loader').slideUp(function () {
591 $(this).remove();
592 });
593
594 return this.constructor.__super__.newMsg.apply(this, arguments);
595 },
596
597 topic: function (topic) {
598 if (typeof topic !== 'string' || !topic) {
599 topic = this.model.get("topic");
600 }
601
602 this.model.addMsg('', '== Topic for ' + this.model.get('name') + ' is: ' + topic, 'topic');
603
604 // If this is the active channel then update the topic bar
605 if (_kiwi.app.panels.active === this) {
606 _kiwi.app.topicbar.setCurrentTopic(this.model.get("topic"));
607 }
608 }
609 });
610
611 // Model for this = _kiwi.model.PanelList
612 _kiwi.view.Tabs = Backbone.View.extend({
613 events: {
614 'click li': 'tabClick',
615 'click li .part': 'partClick'
616 },
617
618 initialize: function () {
619 this.model.on("add", this.panelAdded, this);
620 this.model.on("remove", this.panelRemoved, this);
621 this.model.on("reset", this.render, this);
622
623 this.model.on('active', this.panelActive, this);
624
625 this.tabs_applets = $('ul.applets', this.$el);
626 this.tabs_msg = $('ul.channels', this.$el);
627
628 _kiwi.gateway.on('change:name', function (gateway, new_val) {
629 $('span', this.model.server.tab).text(new_val);
630 }, this);
631 },
632 render: function () {
633 var that = this;
634
635 this.tabs_msg.empty();
636
637 // Add the server tab first
638 this.model.server.tab
639 .data('panel_id', this.model.server.cid)
640 .appendTo(this.tabs_msg);
641
642 // Go through each panel adding its tab
643 this.model.forEach(function (panel) {
644 // If this is the server panel, ignore as it's already added
645 if (panel == that.model.server) return;
646
647 panel.tab
648 .data('panel_id', panel.cid)
649 .appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg);
650 });
651
652 _kiwi.app.view.doLayout();
653 },
654
655 updateTabTitle: function (panel, new_title) {
656 $('span', panel.tab).text(new_title);
657 },
658
659 panelAdded: function (panel) {
660 // Add a tab to the panel
661 panel.tab = $('<li><span>' + (panel.get('title') || panel.get('name')) + '</span><div class="activity"></div></li>');
662
663 if (panel.isServer()) {
664 panel.tab.addClass('server');
665 }
666
667 panel.tab.data('panel_id', panel.cid)
668 .appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg);
669
670 panel.bind('change:title', this.updateTabTitle);
671 _kiwi.app.view.doLayout();
672 },
673 panelRemoved: function (panel) {
674 panel.tab.remove();
675 delete panel.tab;
676
677 _kiwi.app.view.doLayout();
678 },
679
680 panelActive: function (panel, previously_active_panel) {
681 // Remove any existing tabs or part images
682 $('.part', this.$el).remove();
683 this.tabs_applets.children().removeClass('active');
684 this.tabs_msg.children().removeClass('active');
685
686 panel.tab.addClass('active');
687
688 // Only show the part image on non-server tabs
689 if (!panel.isServer()) {
690 panel.tab.append('<span class="part"></span>');
691 }
692 },
693
694 tabClick: function (e) {
695 var tab = $(e.currentTarget);
696
697 var panel = this.model.getByCid(tab.data('panel_id'));
698 if (!panel) {
699 // A panel wasn't found for this tab... wadda fuck
700 return;
701 }
702
703 panel.view.show();
704 },
705
706 partClick: function (e) {
707 var tab = $(e.currentTarget).parent();
708 var panel = this.model.getByCid(tab.data('panel_id'));
709
710 // Only need to part if it's a channel
711 // If the nicklist is empty, we haven't joined the channel as yet
712 if (panel.isChannel() && panel.get('members').models.length > 0) {
713 _kiwi.gateway.part(panel.get('name'));
714 } else {
715 panel.close();
716 }
717 },
718
719 next: function () {
720 var next = _kiwi.app.panels.active.tab.next();
721 if (!next.length) next = $('li:first', this.tabs_msgs);
722
723 next.click();
724 },
725 prev: function () {
726 var prev = _kiwi.app.panels.active.tab.prev();
727 if (!prev.length) prev = $('li:last', this.tabs_msgs);
728
729 prev.click();
730 }
731 });
732
733
734
735 _kiwi.view.TopicBar = Backbone.View.extend({
736 events: {
737 'keydown div': 'process'
738 },
739
740 initialize: function () {
741 _kiwi.app.panels.bind('active', function (active_panel) {
742 // If it's a channel topic, update and make editable
743 if (active_panel.isChannel()) {
744 this.setCurrentTopic(active_panel.get('topic') || '');
745 this.$el.find('div').attr('contentEditable', true);
746
747 } else {
748 // Not a channel topic.. clear and make uneditable
749 this.$el.find('div').attr('contentEditable', false)
750 .text('');
751 }
752 }, this);
753 },
754
755 process: function (ev) {
756 var inp = $(ev.currentTarget),
757 inp_val = inp.text();
758
759 // Only allow topic editing if this is a channel panel
760 if (!_kiwi.app.panels.active.isChannel()) {
761 return false;
762 }
763
764 // If hit return key, update the current topic
765 if (ev.keyCode === 13) {
766 _kiwi.gateway.topic(_kiwi.app.panels.active.get('name'), inp_val);
767 return false;
768 }
769 },
770
771 setCurrentTopic: function (new_topic) {
772 new_topic = new_topic || '';
773
774 // We only want a plain text version
775 $('div', this.$el).html(formatIRCMsg(_.escape(new_topic)));
776 }
777 });
778
779
780
781 _kiwi.view.ControlBox = Backbone.View.extend({
782 events: {
783 'keydown .inp': 'process',
784 'click .nick': 'showNickChange'
785 },
786
787 initialize: function () {
788 var that = this;
789
790 this.buffer = []; // Stores previously run commands
791 this.buffer_pos = 0; // The current position in the buffer
792
793 this.preprocessor = new InputPreProcessor();
794 this.preprocessor.recursive_depth = 5;
795
796 // Hold tab autocomplete data
797 this.tabcomplete = {active: false, data: [], prefix: ''};
798
799 _kiwi.gateway.bind('change:nick', function () {
800 $('.nick', that.$el).text(this.get('nick'));
801 });
802 },
803
804 showNickChange: function (ev) {
805 (new _kiwi.view.NickChangeBox()).render();
806 },
807
808 process: function (ev) {
809 var that = this,
810 inp = $(ev.currentTarget),
811 inp_val = inp.val(),
812 meta;
813
814 if (navigator.appVersion.indexOf("Mac") !== -1) {
815 meta = ev.metaKey;
816 } else {
817 meta = ev.altKey;
818 }
819
820 // If not a tab key, reset the tabcomplete data
821 if (this.tabcomplete.active && ev.keyCode !== 9) {
822 this.tabcomplete.active = false;
823 this.tabcomplete.data = [];
824 this.tabcomplete.prefix = '';
825 }
826
827 switch (true) {
828 case (ev.keyCode === 13): // return
829 inp_val = inp_val.trim();
830
831 if (inp_val) {
832 $.each(inp_val.split('\n'), function (idx, line) {
833 that.processInput(line);
834 });
835
836 this.buffer.push(inp_val);
837 this.buffer_pos = this.buffer.length;
838 }
839
840 inp.val('');
841 return false;
842
843 break;
844
845 case (ev.keyCode === 38): // up
846 if (this.buffer_pos > 0) {
847 this.buffer_pos--;
848 inp.val(this.buffer[this.buffer_pos]);
849 }
850 break;
851
852 case (ev.keyCode === 40): // down
853 if (this.buffer_pos < this.buffer.length) {
854 this.buffer_pos++;
855 inp.val(this.buffer[this.buffer_pos]);
856 }
857 break;
858
859 case (ev.keyCode === 219 && meta): // [ + meta
860 _kiwi.app.panels.view.prev();
861 return false;
862
863 case (ev.keyCode === 221 && meta): // ] + meta
864 _kiwi.app.panels.view.next();
865 return false;
866
867 case (ev.keyCode === 9): // tab
868 this.tabcomplete.active = true;
869 if (_.isEqual(this.tabcomplete.data, [])) {
870 // Get possible autocompletions
871 var ac_data = [];
872 $.each(_kiwi.app.panels.active.get('members').models, function (i, member) {
873 if (!member) return;
874 ac_data.push(member.get('nick'));
875 });
876 ac_data = _.sortBy(ac_data, function (nick) {
877 return nick;
878 });
879 this.tabcomplete.data = ac_data;
880 }
881
882 if (inp_val[inp[0].selectionStart - 1] === ' ') {
883 return false;
884 }
885
886 (function () {
887 var tokens = inp_val.substring(0, inp[0].selectionStart).split(' '),
888 val,
889 p1,
890 newnick,
891 range,
892 nick = tokens[tokens.length - 1];
893 if (this.tabcomplete.prefix === '') {
894 this.tabcomplete.prefix = nick;
895 }
896
897 this.tabcomplete.data = _.select(this.tabcomplete.data, function (n) {
898 return (n.toLowerCase().indexOf(that.tabcomplete.prefix.toLowerCase()) === 0);
899 });
900
901 if (this.tabcomplete.data.length > 0) {
902 p1 = inp[0].selectionStart - (nick.length);
903 val = inp_val.substr(0, p1);
904 newnick = this.tabcomplete.data.shift();
905 this.tabcomplete.data.push(newnick);
906 val += newnick;
907 val += inp_val.substr(inp[0].selectionStart);
908 inp.val(val);
909
910 if (inp[0].setSelectionRange) {
911 inp[0].setSelectionRange(p1 + newnick.length, p1 + newnick.length);
912 } else if (inp[0].createTextRange) { // not sure if this bit is actually needed....
913 range = inp[0].createTextRange();
914 range.collapse(true);
915 range.moveEnd('character', p1 + newnick.length);
916 range.moveStart('character', p1 + newnick.length);
917 range.select();
918 }
919 }
920 }).apply(this);
921 return false;
922 }
923 },
924
925
926 processInput: function (command_raw) {
927 var command, params,
928 pre_processed;
929
930 // The default command
931 if (command_raw[0] !== '/' || command_raw.substr(0, 2) === '//') {
932 // Remove any slash escaping at the start (ie. //)
933 command_raw = command_raw.replace(/^\/\//, '/');
934
935 // Prepend the default command
936 command_raw = '/msg ' + _kiwi.app.panels.active.get('name') + ' ' + command_raw;
937 }
938
939 // Process the raw command for any aliases
940 this.preprocessor.vars.server = _kiwi.gateway.get('name');
941 this.preprocessor.vars.channel = _kiwi.app.panels.active.get('name');
942 this.preprocessor.vars.destination = this.preprocessor.vars.channel;
943 command_raw = this.preprocessor.process(command_raw);
944
945 // Extract the command and parameters
946 params = command_raw.split(' ');
947 if (params[0][0] === '/') {
948 command = params[0].substr(1).toLowerCase();
949 params = params.splice(1, params.length - 1);
950 } else {
951 // Default command
952 command = 'msg';
953 params.unshift(_kiwi.app.panels.active.get('name'));
954 }
955
956 // Trigger the command events
957 this.trigger('command', {command: command, params: params});
958 this.trigger('command:' + command, {command: command, params: params});
959
960 // If we didn't have any listeners for this event, fire a special case
961 // TODO: This feels dirty. Should this really be done..?
962 if (!this._callbacks['command:' + command]) {
963 this.trigger('unknown_command', {command: command, params: params});
964 }
965 }
966 });
967
968
969
970
971 _kiwi.view.StatusMessage = Backbone.View.extend({
972 initialize: function () {
973 this.$el.hide();
974
975 // Timer for hiding the message after X seconds
976 this.tmr = null;
977 },
978
979 text: function (text, opt) {
980 // Defaults
981 opt = opt || {};
982 opt.type = opt.type || '';
983 opt.timeout = opt.timeout || 5000;
984
985 this.$el.text(text).attr('class', opt.type);
986 this.$el.slideDown(_kiwi.app.view.doLayout);
987
988 if (opt.timeout) this.doTimeout(opt.timeout);
989 },
990
991 html: function (html, opt) {
992 // Defaults
993 opt = opt || {};
994 opt.type = opt.type || '';
995 opt.timeout = opt.timeout || 5000;
996
997 this.$el.html(text).attr('class', opt.type);
998 this.$el.slideDown(_kiwi.app.view.doLayout);
999
1000 if (opt.timeout) this.doTimeout(opt.timeout);
1001 },
1002
1003 hide: function () {
1004 this.$el.slideUp(_kiwi.app.view.doLayout);
1005 },
1006
1007 doTimeout: function (length) {
1008 if (this.tmr) clearTimeout(this.tmr);
1009 var that = this;
1010 this.tmr = setTimeout(function () { that.hide(); }, length);
1011 }
1012 });
1013
1014
1015
1016
1017 _kiwi.view.ResizeHandler = Backbone.View.extend({
1018 events: {
1019 'mousedown': 'startDrag',
1020 'mouseup': 'stopDrag'
1021 },
1022
1023 initialize: function () {
1024 this.dragging = false;
1025 this.starting_width = {};
1026
1027 $(window).on('mousemove', $.proxy(this.onDrag, this));
1028 },
1029
1030 startDrag: function (event) {
1031 this.dragging = true;
1032 },
1033
1034 stopDrag: function (event) {
1035 this.dragging = false;
1036 },
1037
1038 onDrag: function (event) {
1039 if (!this.dragging) return;
1040
1041 this.$el.css('left', event.clientX - (this.$el.outerWidth(true) / 2));
1042 $('#memberlists').css('width', this.$el.parent().width() - (this.$el.position().left + this.$el.outerWidth()));
1043 _kiwi.app.view.doLayout();
1044 }
1045 });
1046
1047
1048
1049 _kiwi.view.AppToolbar = Backbone.View.extend({
1050 events: {
1051 'click .settings': 'clickSettings'
1052 },
1053
1054 initialize: function () {
1055 },
1056
1057 clickSettings: function (event) {
1058 _kiwi.app.controlbox.processInput('/settings');
1059 }
1060 });
1061
1062
1063
1064 _kiwi.view.Application = Backbone.View.extend({
1065 initialize: function () {
1066 $(window).resize(this.doLayout);
1067 $('#toolbar').resize(this.doLayout);
1068 $('#controlbox').resize(this.doLayout);
1069
1070 // Change the theme when the config is changed
1071 _kiwi.global.settings.on('change:theme', this.updateTheme, this);
1072 this.updateTheme(getQueryVariable('theme'));
1073
1074 _kiwi.global.settings.on('change:channel_list_style', this.setTabLayout, this);
1075 this.setTabLayout(_kiwi.global.settings.get('channel_list_style'));
1076
1077 this.doLayout();
1078
1079 $(document).keydown(this.setKeyFocus);
1080
1081 // Confirmation require to leave the page
1082 window.onbeforeunload = function () {
1083 if (_kiwi.gateway.isConnected()) {
1084 return 'This will close all KiwiIRC conversations. Are you sure you want to close this window?';
1085 }
1086 };
1087 },
1088
1089
1090
1091 updateTheme: function (theme_name) {
1092 // If called by the settings callback, get the correct new_value
1093 if (theme_name === _kiwi.global.settings) {
1094 theme_name = arguments[1];
1095 }
1096
1097 // If we have no theme specified, get it from the settings
1098 if (!theme_name) theme_name = _kiwi.global.settings.get('theme');
1099
1100 // Clear any current theme
1101 this.$el.removeClass(function (i, css) {
1102 return (css.match(/\btheme_\S+/g) || []).join(' ');
1103 });
1104
1105 // Apply the new theme
1106 this.$el.addClass('theme_' + (theme_name || 'relaxed'));
1107 },
1108
1109
1110 setTabLayout: function (layout_style) {
1111 // If called by the settings callback, get the correct new_value
1112 if (layout_style === _kiwi.global.settings) {
1113 layout_style = arguments[1];
1114 }
1115
1116 if (layout_style == 'list') {
1117 this.$el.addClass('chanlist_treeview');
1118 } else {
1119 this.$el.removeClass('chanlist_treeview');
1120 }
1121
1122 this.doLayout();
1123 },
1124
1125
1126 // Globally shift focus to the command input box on a keypress
1127 setKeyFocus: function (ev) {
1128 // If we're copying text, don't shift focus
1129 if (ev.ctrlKey || ev.altKey || ev.metaKey) {
1130 return;
1131 }
1132
1133 // If we're typing into an input box somewhere, ignore
1134 if ((ev.target.tagName.toLowerCase() === 'input') || (ev.target.tagName.toLowerCase() === 'textarea') || $(ev.target).attr('contenteditable')) {
1135 return;
1136 }
1137
1138 $('#controlbox .inp').focus();
1139 },
1140
1141
1142 doLayout: function () {
1143 var el_kiwi = $('#kiwi');
1144 var el_panels = $('#panels');
1145 var el_memberlists = $('#memberlists');
1146 var el_toolbar = $('#toolbar');
1147 var el_controlbox = $('#controlbox');
1148 var el_resize_handle = $('#memberlists_resize_handle');
1149
1150 var css_heights = {
1151 top: el_toolbar.outerHeight(true),
1152 bottom: el_controlbox.outerHeight(true)
1153 };
1154
1155
1156 // If any elements are not visible, full size the panals instead
1157 if (!el_toolbar.is(':visible')) {
1158 css_heights.top = 0;
1159 }
1160
1161 if (!el_controlbox.is(':visible')) {
1162 css_heights.bottom = 0;
1163 }
1164
1165 // Apply the CSS sizes
1166 el_panels.css(css_heights);
1167 el_memberlists.css(css_heights);
1168 el_resize_handle.css(css_heights);
1169
1170 // If we have channel tabs on the side, adjust the height
1171 if (el_kiwi.hasClass('chanlist_treeview')) {
1172 $('#kiwi #tabs').css(css_heights);
1173 }
1174
1175 // Determine if we have a narrow window (mobile/tablet/or even small desktop window)
1176 if (el_kiwi.outerWidth() < 400) {
1177 el_kiwi.addClass('narrow');
1178 } else {
1179 el_kiwi.removeClass('narrow');
1180 }
1181
1182 // Set the panels width depending on the memberlist visibility
1183 if (el_memberlists.css('display') != 'none') {
1184 // Panels to the side of the memberlist
1185 el_panels.css('right', el_memberlists.outerWidth(true));
1186 // The resize handle sits overlapping the panels and memberlist
1187 el_resize_handle.css('left', el_memberlists.position().left - (el_resize_handle.outerWidth(true) / 2));
1188 } else {
1189 // Memberlist is hidden so panels to the right edge
1190 el_panels.css('right', 0);
1191 // And move the handle just out of sight to the right
1192 el_resize_handle.css('left', el_panels.outerWidth(true));
1193 }
1194 },
1195
1196
1197 alertWindow: function (title) {
1198 if (!this.alertWindowTimer) {
1199 this.alertWindowTimer = new (function () {
1200 var that = this;
1201 var tmr;
1202 var has_focus = true;
1203 var state = 0;
1204 var default_title = 'Kiwi IRC';
1205 var title = 'Kiwi IRC';
1206
1207 this.setTitle = function (new_title) {
1208 new_title = new_title || default_title;
1209 window.document.title = new_title;
1210 return new_title;
1211 };
1212
1213 this.start = function (new_title) {
1214 // Don't alert if we already have focus
1215 if (has_focus) return;
1216
1217 title = new_title;
1218 if (tmr) return;
1219 tmr = setInterval(this.update, 1000);
1220 };
1221
1222 this.stop = function () {
1223 // Stop the timer and clear the title
1224 if (tmr) clearInterval(tmr);
1225 tmr = null;
1226 this.setTitle();
1227
1228 // Some browsers don't always update the last title correctly
1229 // Wait a few seconds and then reset
1230 setTimeout(this.reset, 2000);
1231 };
1232
1233 this.reset = function () {
1234 if (tmr) return;
1235 that.setTitle();
1236 };
1237
1238
1239 this.update = function () {
1240 if (state === 0) {
1241 that.setTitle(title);
1242 state = 1;
1243 } else {
1244 that.setTitle();
1245 state = 0;
1246 }
1247 };
1248
1249 $(window).focus(function (event) {
1250 has_focus = true;
1251 that.stop();
1252
1253 // Some browsers don't always update the last title correctly
1254 // Wait a few seconds and then reset
1255 setTimeout(that.reset, 2000);
1256 });
1257
1258 $(window).blur(function (event) {
1259 has_focus = false;
1260 });
1261 })();
1262 }
1263
1264 this.alertWindowTimer.start(title);
1265 },
1266
1267
1268 barsHide: function (instant) {
1269 var that = this;
1270
1271 if (!instant) {
1272 $('#toolbar').slideUp({queue: false, duration: 400, step: this.doLayout});
1273 $('#controlbox').slideUp({queue: false, duration: 400, step: this.doLayout});
1274 } else {
1275 $('#toolbar').slideUp(0);
1276 $('#controlbox').slideUp(0);
1277 this.doLayout();
1278 }
1279 },
1280
1281 barsShow: function (instant) {
1282 var that = this;
1283
1284 if (!instant) {
1285 $('#toolbar').slideDown({queue: false, duration: 400, step: this.doLayout});
1286 $('#controlbox').slideDown({queue: false, duration: 400, step: this.doLayout});
1287 } else {
1288 $('#toolbar').slideDown(0);
1289 $('#controlbox').slideDown(0);
1290 this.doLayout();
1291 }
1292 }
1293 });
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303 _kiwi.view.MediaMessage = Backbone.View.extend({
1304 events: {
1305 'click .media_close': 'close'
1306 },
1307
1308 initialize: function () {
1309 // Get the URL from the data
1310 this.url = this.$el.data('url');
1311 },
1312
1313 // Close the media content and remove it from display
1314 close: function () {
1315 var that = this;
1316 this.$content.slideUp('fast', function () {
1317 that.$content.remove();
1318 });
1319 },
1320
1321 // Open the media content within its wrapper
1322 open: function () {
1323 // Create the content div if we haven't already
1324 if (!this.$content) {
1325 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>');
1326 this.$content.find('.content').append(this.mediaTypes[this.$el.data('type')].apply(this, []) || 'Not found :(');
1327 }
1328
1329 // Now show the content if not already
1330 if (!this.$content.is(':visible')) {
1331 // Hide it first so the slideDown always plays
1332 this.$content.hide();
1333
1334 // Add the media content and slide it into view
1335 this.$el.append(this.$content);
1336 this.$content.slideDown();
1337 }
1338 },
1339
1340
1341
1342 // Generate the media content for each recognised type
1343 mediaTypes: {
1344 twitter: function () {
1345 var tweet_id = this.$el.data('tweetid');
1346 var that = this;
1347
1348 $.getJSON('https://api.twitter.com/1/statuses/oembed.json?id=' + tweet_id + '&callback=?', function (data) {
1349 that.$content.find('.content').html(data.html);
1350 });
1351
1352 return $('<div>Loading tweet..</div>');
1353 },
1354
1355
1356 image: function () {
1357 return $('<a href="' + this.url + '" target="_blank"><img height="100" src="' + this.url + '" /></a>');
1358 },
1359
1360
1361 reddit: function () {
1362 var that = this;
1363 var matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(this.url);
1364
1365 $.getJSON('http://www.' + matches[0] + '.json?jsonp=?', function (data) {
1366 console.log('Loaded reddit data', data);
1367 var post = data[0].data.children[0].data;
1368 var thumb = '';
1369
1370 // Show a thumbnail if there is one
1371 if (post.thumbnail) {
1372 //post.thumbnail = 'http://www.eurotunnel.com/uploadedImages/commercial/back-steps-icon-arrow.png';
1373
1374 // Hide the thumbnail if an over_18 image
1375 if (post.over_18) {
1376 thumb = '<span class="thumbnail_nsfw" onclick="$(this).find(\'p\').remove(); $(this).find(\'img\').css(\'visibility\', \'visible\');">';
1377 thumb += '<p style="font-size:0.9em;line-height:1.2em;cursor:pointer;">Show<br />NSFW</p>';
1378 thumb += '<img src="' + post.thumbnail + '" class="thumbnail" style="visibility:hidden;" />';
1379 thumb += '</span>';
1380 } else {
1381 thumb = '<img src="' + post.thumbnail + '" class="thumbnail" />';
1382 }
1383 }
1384
1385 // Build the template string up
1386 var tmpl = '<div>' + thumb + '<b><%- title %></b><br />Posted by <%- author %>. &nbsp;&nbsp; ';
1387 tmpl += '<i class="icon-arrow-up"></i> <%- ups %> &nbsp;&nbsp; <i class="icon-arrow-down"></i> <%- downs %><br />';
1388 tmpl += '<%- num_comments %> comments made. <a href="http://www.reddit.com<%- permalink %>">View post</a></div>';
1389
1390 that.$content.find('.content').html(_.template(tmpl, post));
1391 });
1392
1393 return $('<div>Loading Reddit thread..</div>');
1394 }
1395 }
1396
1397 }, {
1398
1399 // Build the closed media HTML from a URL
1400 buildHtml: function (url) {
1401 var html = '', matches;
1402
1403 // Is it an image?
1404 if (url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
1405 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>';
1406 }
1407
1408 // Is it a tweet?
1409 matches = (/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/ig).exec(url);
1410 if (matches) {
1411 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>';
1412 }
1413
1414 // Is reddit?
1415 matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(url);
1416 if (matches) {
1417 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>';
1418 }
1419
1420 return html;
1421 }
1422 });