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