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