Merged in lodash from karbassi
[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 'mouseenter .msg .nick': 'msgEnter',
253 'mouseleave .msg .nick': 'msgLeave'
254 },
255
256 initialize: function (options) {
257 this.initializePanel(options);
258 },
259
260 initializePanel: function (options) {
261 this.$el.css('display', 'none');
262 options = options || {};
263
264 // Containing element for this panel
265 if (options.container) {
266 this.$container = $(options.container);
267 } else {
268 this.$container = $('#panels .container1');
269 }
270
271 this.$el.appendTo(this.$container);
272
273 this.alert_level = 0;
274
275 this.model.bind('msg', this.newMsg, this);
276 this.msg_count = 0;
277
278 this.model.set({"view": this}, {"silent": true});
279 },
280
281 render: function () {
282 this.$el.empty();
283 this.model.get("backscroll").forEach(this.newMsg);
284 },
285 newMsg: function (msg) {
286 // TODO: make sure that the message pane is scrolled to the bottom (Or do we? ~Darren)
287 var re, line_msg, $this = this.$el,
288 nick_colour_hex, nick_hex, is_highlight, msg_css_classes = '';
289
290 // Nick highlight detecting
291 if ((new RegExp('\\b' + _kiwi.gateway.get('nick') + '\\b', 'i')).test(msg.msg)) {
292 is_highlight = true;
293 msg_css_classes += ' highlight';
294 }
295
296 // Escape any HTML that may be in here
297 msg.msg = $('<div />').text(msg.msg).html();
298
299 // Make the channels clickable
300 re = new RegExp('\\B([' + _kiwi.gateway.get('channel_prefix') + '][^ ,.\\007]+)', 'g');
301 msg.msg = msg.msg.replace(re, function (match) {
302 return '<a class="chan">' + match + '</a>';
303 });
304
305
306 // Make links clickable
307 msg.msg = msg.msg.replace(/((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]*))?/gi, function (url) {
308 var nice;
309
310 // Add the http is no protoocol was found
311 if (url.match(/^www\./)) {
312 url = 'http://' + url;
313 }
314
315 nice = url;
316 if (nice.length > 100) {
317 nice = nice.substr(0, 100) + '...';
318 }
319
320 return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url + '">' + nice + '</a>';
321 });
322
323
324 // Convert IRC formatting into HTML formatting
325 msg.msg = formatIRCMsg(msg.msg);
326
327
328 // Add some colours to the nick (Method based on IRSSIs nickcolor.pl)
329 nick_colour_hex = (function (nick) {
330 var nick_int = 0, rgb;
331
332 _.map(nick.split(''), function (i) { nick_int += i.charCodeAt(0); });
333 rgb = hsl2rgb(nick_int % 255, 70, 35);
334 rgb = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);
335
336 return '#' + rgb.toString(16);
337 })(msg.nick);
338
339 msg.nick_style = 'color:' + nick_colour_hex + ';';
340
341 // Generate a hex string from the nick to be used as a CSS class name
342 nick_hex = msg.nick_css_class = '';
343 if (msg.nick) {
344 _.map(msg.nick.split(''), function (char) {
345 nick_hex += char.charCodeAt(0).toString(16);
346 });
347 msg_css_classes += ' nick_' + nick_hex;
348 }
349
350 // Build up and add the line
351 msg.msg_css_classes = msg_css_classes;
352 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>';
353 $this.append(_.template(line_msg, msg));
354
355 // Activity/alerts based on the type of new message
356 if (msg.type.match(/^action /)) {
357 this.alert('action');
358 } else if (is_highlight) {
359 _kiwi.app.view.alertWindow('* People are talking!');
360 this.alert('highlight');
361 } else {
362 // If this is the active panel, send an alert out
363 if (this.model.isActive()) {
364 _kiwi.app.view.alertWindow('* People are talking!');
365 }
366 this.alert('activity');
367 }
368
369 this.scrollToBottom();
370
371 // Make sure our DOM isn't getting too large (Acts as scrollback)
372 this.msg_count++;
373 if (this.msg_count > (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250)) {
374 $('.msg:first', this.$el).remove();
375 this.msg_count--;
376 }
377 },
378 chanClick: function (event) {
379 if (event.target) {
380 _kiwi.gateway.join($(event.target).text());
381 } else {
382 // IE...
383 _kiwi.gateway.join($(event.srcElement).text());
384 }
385 },
386
387 msgEnter: function (event) {
388 var nick_class;
389
390 // Find a valid class that this element has
391 _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
392 if (css_class.match(/^nick_[a-z0-9]+/i)) {
393 nick_class = css_class;
394 }
395 });
396
397 // If no class was found..
398 if (!nick_class) return;
399
400 $('.'+nick_class).addClass('global_nick_highlight');
401 },
402
403 msgLeave: function (event) {
404 var nick_class;
405
406 // Find a valid class that this element has
407 _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
408 if (css_class.match(/^nick_[a-z0-9]+/i)) {
409 nick_class = css_class;
410 }
411 });
412
413 // If no class was found..
414 if (!nick_class) return;
415
416 $('.'+nick_class).removeClass('global_nick_highlight');
417 },
418
419 show: function () {
420 var $this = this.$el;
421
422 // Hide all other panels and show this one
423 this.$container.children().css('display', 'none');
424 $this.css('display', 'block');
425
426 // Show this panels memberlist
427 var members = this.model.get("members");
428 if (members) {
429 $('#memberlists').show();
430 members.view.show();
431 } else {
432 // Memberlist not found for this panel, hide any active ones
433 $('#memberlists').hide().children().removeClass('active');
434 }
435
436 _kiwi.app.view.doLayout();
437 this.alert('none');
438
439 this.trigger('active', this.model);
440 _kiwi.app.panels.trigger('active', this.model);
441
442 this.scrollToBottom(true);
443 },
444
445
446 alert: function (level) {
447 // No need to highlight if this si the active panel
448 if (this.model == _kiwi.app.panels.active) return;
449
450 var types, type_idx;
451 types = ['none', 'action', 'activity', 'highlight'];
452
453 // Default alert level
454 level = level || 'none';
455
456 // If this alert level does not exist, assume clearing current level
457 type_idx = _.indexOf(types, level);
458 if (!type_idx) {
459 level = 'none';
460 type_idx = 0;
461 }
462
463 // Only 'upgrade' the alert. Never down (unless clearing)
464 if (type_idx !== 0 && type_idx <= this.alert_level) {
465 return;
466 }
467
468 // Clear any existing levels
469 this.model.tab.removeClass(function (i, css) {
470 return (css.match(/\balert_\S+/g) || []).join(' ');
471 });
472
473 // Add the new level if there is one
474 if (level !== 'none') {
475 this.model.tab.addClass('alert_' + level);
476 }
477
478 this.alert_level = type_idx;
479 },
480
481
482 // Scroll to the bottom of the panel
483 scrollToBottom: function (force_down) {
484 // If this isn't the active panel, don't scroll
485 if (this.model !== _kiwi.app.panels.active) return;
486
487 // Don't scroll down if we're scrolled up the panel a little
488 if (force_down || this.$container.scrollTop() + this.$container.height() > this.$el.outerHeight() - 150) {
489 this.$container[0].scrollTop = this.$container[0].scrollHeight;
490 }
491 }
492 });
493
494 _kiwi.view.Applet = _kiwi.view.Panel.extend({
495 className: 'applet',
496 initialize: function (options) {
497 this.initializePanel(options);
498 }
499 });
500
501 _kiwi.view.Channel = _kiwi.view.Panel.extend({
502 initialize: function (options) {
503 this.initializePanel(options);
504 this.model.bind('change:topic', this.topic, this);
505 },
506
507 topic: function (topic) {
508 if (typeof topic !== 'string' || !topic) {
509 topic = this.model.get("topic");
510 }
511
512 this.model.addMsg('', '== Topic for ' + this.model.get('name') + ' is: ' + topic, 'topic');
513
514 // If this is the active channel then update the topic bar
515 if (_kiwi.app.panels.active === this) {
516 _kiwi.app.topicbar.setCurrentTopic(this.model.get("topic"));
517 }
518 }
519 });
520
521 // Model for this = _kiwi.model.PanelList
522 _kiwi.view.Tabs = Backbone.View.extend({
523 events: {
524 'click li': 'tabClick',
525 'click li .part': 'partClick'
526 },
527
528 initialize: function () {
529 this.model.on("add", this.panelAdded, this);
530 this.model.on("remove", this.panelRemoved, this);
531 this.model.on("reset", this.render, this);
532
533 this.model.on('active', this.panelActive, this);
534
535 this.tabs_applets = $('ul.applets', this.$el);
536 this.tabs_msg = $('ul.channels', this.$el);
537
538 _kiwi.gateway.on('change:name', function (gateway, new_val) {
539 $('span', this.model.server.tab).text(new_val);
540 }, this);
541 },
542 render: function () {
543 var that = this;
544
545 this.tabs_msg.empty();
546
547 // Add the server tab first
548 this.model.server.tab
549 .data('panel_id', this.model.server.cid)
550 .appendTo(this.tabs_msg);
551
552 // Go through each panel adding its tab
553 this.model.forEach(function (panel) {
554 // If this is the server panel, ignore as it's already added
555 if (panel == that.model.server) return;
556
557 panel.tab
558 .data('panel_id', panel.cid)
559 .appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg);
560 });
561
562 _kiwi.app.view.doLayout();
563 },
564
565 updateTabTitle: function (panel, new_title) {
566 $('span', panel.tab).text(new_title);
567 },
568
569 panelAdded: function (panel) {
570 // Add a tab to the panel
571 panel.tab = $('<li><span>' + (panel.get('title') || panel.get('name')) + '</span></li>');
572
573 if (panel.isServer()) {
574 panel.tab.addClass('server');
575 }
576
577 panel.tab.data('panel_id', panel.cid)
578 .appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg);
579
580 panel.bind('change:title', this.updateTabTitle);
581 _kiwi.app.view.doLayout();
582 },
583 panelRemoved: function (panel) {
584 panel.tab.remove();
585 delete panel.tab;
586
587 _kiwi.app.view.doLayout();
588 },
589
590 panelActive: function (panel) {
591 // Remove any existing tabs or part images
592 $('.part', this.$el).remove();
593 this.tabs_applets.children().removeClass('active');
594 this.tabs_msg.children().removeClass('active');
595
596 panel.tab.addClass('active');
597
598 // Only show the part image on non-server tabs
599 if (!panel.isServer()) {
600 panel.tab.append('<span class="part"></span>');
601 }
602 },
603
604 tabClick: function (e) {
605 var tab = $(e.currentTarget);
606
607 var panel = this.model.getByCid(tab.data('panel_id'));
608 if (!panel) {
609 // A panel wasn't found for this tab... wadda fuck
610 return;
611 }
612
613 panel.view.show();
614 },
615
616 partClick: function (e) {
617 var tab = $(e.currentTarget).parent();
618 var panel = this.model.getByCid(tab.data('panel_id'));
619
620 // Only need to part if it's a channel
621 // If the nicklist is empty, we haven't joined the channel as yet
622 if (panel.isChannel() && panel.get('members').models.length > 0) {
623 _kiwi.gateway.part(panel.get('name'));
624 } else {
625 panel.close();
626 }
627 },
628
629 next: function () {
630 var next = _kiwi.app.panels.active.tab.next();
631 if (!next.length) next = $('li:first', this.tabs_msgs);
632
633 next.click();
634 },
635 prev: function () {
636 var prev = _kiwi.app.panels.active.tab.prev();
637 if (!prev.length) prev = $('li:last', this.tabs_msgs);
638
639 prev.click();
640 }
641 });
642
643
644
645 _kiwi.view.TopicBar = Backbone.View.extend({
646 events: {
647 'keydown div': 'process'
648 },
649
650 initialize: function () {
651 _kiwi.app.panels.bind('active', function (active_panel) {
652 // If it's a channel topic, update and make editable
653 if (active_panel.isChannel()) {
654 this.setCurrentTopic(active_panel.get('topic') || '');
655 this.$el.find('div').attr('contentEditable', true);
656
657 } else {
658 // Not a channel topic.. clear and make uneditable
659 this.$el.find('div').attr('contentEditable', false)
660 .text('');
661 }
662 }, this);
663 },
664
665 process: function (ev) {
666 var inp = $(ev.currentTarget),
667 inp_val = inp.text();
668
669 // Only allow topic editing if this is a channel panel
670 if (!_kiwi.app.panels.active.isChannel()) {
671 return false;
672 }
673
674 // If hit return key, update the current topic
675 if (ev.keyCode === 13) {
676 _kiwi.gateway.topic(_kiwi.app.panels.active.get('name'), inp_val);
677 return false;
678 }
679 },
680
681 setCurrentTopic: function (new_topic) {
682 new_topic = new_topic || '';
683
684 // We only want a plain text version
685 $('div', this.$el).html(formatIRCMsg(_.escape(new_topic)));
686 }
687 });
688
689
690
691 _kiwi.view.ControlBox = Backbone.View.extend({
692 events: {
693 'keydown .inp': 'process',
694 'click .nick': 'showNickChange'
695 },
696
697 initialize: function () {
698 var that = this;
699
700 this.buffer = []; // Stores previously run commands
701 this.buffer_pos = 0; // The current position in the buffer
702
703 this.preprocessor = new InputPreProcessor();
704 this.preprocessor.recursive_depth = 5;
705
706 // Hold tab autocomplete data
707 this.tabcomplete = {active: false, data: [], prefix: ''};
708
709 _kiwi.gateway.bind('change:nick', function () {
710 $('.nick', that.$el).text(this.get('nick'));
711 });
712 },
713
714 showNickChange: function (ev) {
715 (new _kiwi.view.NickChangeBox()).render();
716 },
717
718 process: function (ev) {
719 var that = this,
720 inp = $(ev.currentTarget),
721 inp_val = inp.val(),
722 meta;
723
724 if (navigator.appVersion.indexOf("Mac") !== -1) {
725 meta = ev.ctrlKey;
726 } else {
727 meta = ev.altKey;
728 }
729
730 // If not a tab key, reset the tabcomplete data
731 if (this.tabcomplete.active && ev.keyCode !== 9) {
732 this.tabcomplete.active = false;
733 this.tabcomplete.data = [];
734 this.tabcomplete.prefix = '';
735 }
736
737 switch (true) {
738 case (ev.keyCode === 13): // return
739 inp_val = inp_val.trim();
740
741 if (inp_val) {
742 $.each(inp_val.split('\n'), function (idx, line) {
743 that.processInput(line);
744 });
745
746 this.buffer.push(inp_val);
747 this.buffer_pos = this.buffer.length;
748 }
749
750 inp.val('');
751 return false;
752
753 break;
754
755 case (ev.keyCode === 38): // up
756 if (this.buffer_pos > 0) {
757 this.buffer_pos--;
758 inp.val(this.buffer[this.buffer_pos]);
759 }
760 break;
761
762 case (ev.keyCode === 40): // down
763 if (this.buffer_pos < this.buffer.length) {
764 this.buffer_pos++;
765 inp.val(this.buffer[this.buffer_pos]);
766 }
767 break;
768
769 case (ev.keyCode === 37 && meta): // left
770 _kiwi.app.panels.view.prev();
771 return false;
772
773 case (ev.keyCode === 39 && meta): // right
774 _kiwi.app.panels.view.next();
775 return false;
776
777 case (ev.keyCode === 9): // tab
778 this.tabcomplete.active = true;
779 if (_.isEqual(this.tabcomplete.data, [])) {
780 // Get possible autocompletions
781 var ac_data = [];
782 $.each(_kiwi.app.panels.active.get('members').models, function (i, member) {
783 if (!member) return;
784 ac_data.push(member.get('nick'));
785 });
786 ac_data = _.sortBy(ac_data, function (nick) {
787 return nick;
788 });
789 this.tabcomplete.data = ac_data;
790 }
791
792 if (inp_val[inp[0].selectionStart - 1] === ' ') {
793 return false;
794 }
795
796 (function () {
797 var tokens = inp_val.substring(0, inp[0].selectionStart).split(' '),
798 val,
799 p1,
800 newnick,
801 range,
802 nick = tokens[tokens.length - 1];
803 if (this.tabcomplete.prefix === '') {
804 this.tabcomplete.prefix = nick;
805 }
806
807 this.tabcomplete.data = _.select(this.tabcomplete.data, function (n) {
808 return (n.toLowerCase().indexOf(that.tabcomplete.prefix.toLowerCase()) === 0);
809 });
810
811 if (this.tabcomplete.data.length > 0) {
812 p1 = inp[0].selectionStart - (nick.length);
813 val = inp_val.substr(0, p1);
814 newnick = this.tabcomplete.data.shift();
815 this.tabcomplete.data.push(newnick);
816 val += newnick;
817 val += inp_val.substr(inp[0].selectionStart);
818 inp.val(val);
819
820 if (inp[0].setSelectionRange) {
821 inp[0].setSelectionRange(p1 + newnick.length, p1 + newnick.length);
822 } else if (inp[0].createTextRange) { // not sure if this bit is actually needed....
823 range = inp[0].createTextRange();
824 range.collapse(true);
825 range.moveEnd('character', p1 + newnick.length);
826 range.moveStart('character', p1 + newnick.length);
827 range.select();
828 }
829 }
830 }).apply(this);
831 return false;
832 }
833 },
834
835
836 processInput: function (command_raw) {
837 var command, params,
838 pre_processed;
839
840 // The default command
841 if (command_raw[0] !== '/' || command_raw.substr(0, 2) === '//') {
842 // Remove any slash escaping at the start (ie. //)
843 command_raw = command_raw.replace(/^\/\//, '/');
844
845 // Prepend the default command
846 command_raw = '/msg ' + _kiwi.app.panels.active.get('name') + ' ' + command_raw;
847 }
848
849 // Process the raw command for any aliases
850 this.preprocessor.vars.server = _kiwi.gateway.get('name');
851 this.preprocessor.vars.channel = _kiwi.app.panels.active.get('name');
852 this.preprocessor.vars.destination = this.preprocessor.vars.channel;
853 command_raw = this.preprocessor.process(command_raw);
854
855 // Extract the command and parameters
856 params = command_raw.split(' ');
857 if (params[0][0] === '/') {
858 command = params[0].substr(1).toLowerCase();
859 params = params.splice(1, params.length - 1);
860 } else {
861 // Default command
862 command = 'msg';
863 params.unshift(_kiwi.app.panels.active.get('name'));
864 }
865
866 // Trigger the command events
867 this.trigger('command', {command: command, params: params});
868 this.trigger('command_' + command, {command: command, params: params});
869
870 // If we didn't have any listeners for this event, fire a special case
871 // TODO: This feels dirty. Should this really be done..?
872 if (!this._callbacks['command_' + command]) {
873 this.trigger('unknown_command', {command: command, params: params});
874 }
875 }
876 });
877
878
879
880
881 _kiwi.view.StatusMessage = Backbone.View.extend({
882 initialize: function () {
883 this.$el.hide();
884
885 // Timer for hiding the message after X seconds
886 this.tmr = null;
887 },
888
889 text: function (text, opt) {
890 // Defaults
891 opt = opt || {};
892 opt.type = opt.type || '';
893 opt.timeout = opt.timeout || 5000;
894
895 this.$el.text(text).attr('class', opt.type);
896 this.$el.slideDown(_kiwi.app.view.doLayout);
897
898 if (opt.timeout) this.doTimeout(opt.timeout);
899 },
900
901 html: function (html, opt) {
902 // Defaults
903 opt = opt || {};
904 opt.type = opt.type || '';
905 opt.timeout = opt.timeout || 5000;
906
907 this.$el.html(text).attr('class', opt.type);
908 this.$el.slideDown(_kiwi.app.view.doLayout);
909
910 if (opt.timeout) this.doTimeout(opt.timeout);
911 },
912
913 hide: function () {
914 this.$el.slideUp(_kiwi.app.view.doLayout);
915 },
916
917 doTimeout: function (length) {
918 if (this.tmr) clearTimeout(this.tmr);
919 var that = this;
920 this.tmr = setTimeout(function () { that.hide(); }, length);
921 }
922 });
923
924
925
926
927 _kiwi.view.ResizeHandler = Backbone.View.extend({
928 events: {
929 'mousedown': 'startDrag',
930 'mouseup': 'stopDrag'
931 },
932
933 initialize: function () {
934 this.dragging = false;
935 this.starting_width = {};
936
937 $(window).on('mousemove', $.proxy(this.onDrag, this));
938 },
939
940 startDrag: function (event) {
941 this.dragging = true;
942 },
943
944 stopDrag: function (event) {
945 this.dragging = false;
946 },
947
948 onDrag: function (event) {
949 if (!this.dragging) return;
950
951 this.$el.css('left', event.clientX - (this.$el.outerWidth(true) / 2));
952 $('#memberlists').css('width', this.$el.parent().width() - (this.$el.position().left + this.$el.outerWidth()));
953 _kiwi.app.view.doLayout();
954 }
955 });
956
957
958
959 _kiwi.view.AppToolbar = Backbone.View.extend({
960 events: {
961 'click .settings': 'clickSettings'
962 },
963
964 initialize: function () {
965 },
966
967 clickSettings: function (event) {
968 _kiwi.app.controlbox.processInput('/settings');
969 }
970 });
971
972
973
974 _kiwi.view.Application = Backbone.View.extend({
975 initialize: function () {
976 $(window).resize(this.doLayout);
977 $('#toolbar').resize(this.doLayout);
978 $('#controlbox').resize(this.doLayout);
979
980 // Change the theme when the config is changed
981 _kiwi.global.settings.on('change:theme', this.updateTheme, this);
982 this.updateTheme();
983
984 this.doLayout();
985
986 $(document).keydown(this.setKeyFocus);
987
988 // Confirmation require to leave the page
989 window.onbeforeunload = function () {
990 if (_kiwi.gateway.isConnected()) {
991 return 'This will close all KiwiIRC conversations. Are you sure you want to close this window?';
992 }
993 };
994 },
995
996
997
998 updateTheme: function (theme_name) {
999 // If called by the settings callback, get the correct new_value
1000 if (theme_name === _kiwi.global.settings) {
1001 theme_name = arguments[1];
1002 }
1003
1004 // If we have no theme specified, get it from the settings
1005 if (!theme_name) theme_name = _kiwi.global.settings.get('theme');
1006
1007 // Clear any current theme
1008 this.$el.removeClass(function (i, css) {
1009 return (css.match (/\btheme_\S+/g) || []).join(' ');
1010 });
1011
1012 // Apply the new theme
1013 this.$el.addClass('theme_' + (theme_name || 'relaxed'));
1014 },
1015
1016
1017 // Globally shift focus to the command input box on a keypress
1018 setKeyFocus: function (ev) {
1019 // If we're copying text, don't shift focus
1020 if (ev.ctrlKey || ev.altKey || ev.metaKey) {
1021 return;
1022 }
1023
1024 // If we're typing into an input box somewhere, ignore
1025 if ((ev.target.tagName.toLowerCase() === 'input') || $(ev.target).attr('contenteditable')) {
1026 return;
1027 }
1028
1029 $('#controlbox .inp').focus();
1030 },
1031
1032
1033 doLayout: function () {
1034 var el_panels = $('#panels');
1035 var el_memberlists = $('#memberlists');
1036 var el_toolbar = $('#toolbar');
1037 var el_controlbox = $('#controlbox');
1038 var el_resize_handle = $('#memberlists_resize_handle');
1039
1040 var css_heights = {
1041 top: el_toolbar.outerHeight(true),
1042 bottom: el_controlbox.outerHeight(true)
1043 };
1044
1045
1046 // If any elements are not visible, full size the panals instead
1047 if (!el_toolbar.is(':visible')) {
1048 css_heights.top = 0;
1049 }
1050
1051 if (!el_controlbox.is(':visible')) {
1052 css_heights.bottom = 0;
1053 }
1054
1055 // Apply the CSS sizes
1056 el_panels.css(css_heights);
1057 el_memberlists.css(css_heights);
1058 el_resize_handle.css(css_heights);
1059
1060 // Set the panels width depending on the memberlist visibility
1061 if (el_memberlists.css('display') != 'none') {
1062 // Handle + panels to the side of the memberlist
1063 el_panels.css('right', el_memberlists.outerWidth(true) + el_resize_handle.outerWidth(true));
1064 el_resize_handle.css('left', el_memberlists.position().left - el_resize_handle.outerWidth(true));
1065 } else {
1066 // Memberlist is hidden so handle + panels to the right edge
1067 el_panels.css('right', el_resize_handle.outerWidth(true));
1068 el_resize_handle.css('left', el_panels.outerWidth(true));
1069 }
1070 },
1071
1072
1073 alertWindow: function (title) {
1074 if (!this.alertWindowTimer) {
1075 this.alertWindowTimer = new (function () {
1076 var that = this;
1077 var tmr;
1078 var has_focus = true;
1079 var state = 0;
1080 var default_title = 'Kiwi IRC';
1081 var title = 'Kiwi IRC';
1082
1083 this.setTitle = function (new_title) {
1084 new_title = new_title || default_title;
1085 window.document.title = new_title;
1086 return new_title;
1087 };
1088
1089 this.start = function (new_title) {
1090 // Don't alert if we already have focus
1091 if (has_focus) return;
1092
1093 title = new_title;
1094 if (tmr) return;
1095 tmr = setInterval(this.update, 1000);
1096 };
1097
1098 this.stop = function () {
1099 // Stop the timer and clear the title
1100 if (tmr) clearInterval(tmr);
1101 tmr = null;
1102 this.setTitle();
1103
1104 // Some browsers don't always update the last title correctly
1105 // Wait a few seconds and then reset
1106 setTimeout(this.reset, 2000);
1107 };
1108
1109 this.reset = function () {
1110 if (tmr) return;
1111 that.setTitle();
1112 };
1113
1114
1115 this.update = function () {
1116 if (state === 0) {
1117 that.setTitle(title);
1118 state = 1;
1119 } else {
1120 that.setTitle();
1121 state = 0;
1122 }
1123 };
1124
1125 $(window).focus(function (event) {
1126 has_focus = true;
1127 that.stop();
1128
1129 // Some browsers don't always update the last title correctly
1130 // Wait a few seconds and then reset
1131 setTimeout(that.reset, 2000);
1132 });
1133
1134 $(window).blur(function (event) {
1135 has_focus = false;
1136 });
1137 })();
1138 }
1139
1140 this.alertWindowTimer.start(title);
1141 },
1142
1143
1144 barsHide: function (instant) {
1145 var that = this;
1146
1147 if (!instant) {
1148 $('#toolbar').slideUp({queue: false, duration: 400, step: this.doLayout});
1149 $('#controlbox').slideUp({queue: false, duration: 400, step: this.doLayout});
1150 } else {
1151 $('#toolbar').slideUp(0);
1152 $('#controlbox').slideUp(0);
1153 this.doLayout();
1154 }
1155 },
1156
1157 barsShow: function (instant) {
1158 var that = this;
1159
1160 if (!instant) {
1161 $('#toolbar').slideDown({queue: false, duration: 400, step: this.doLayout});
1162 $('#controlbox').slideDown({queue: false, duration: 400, step: this.doLayout});
1163 } else {
1164 $('#toolbar').slideDown(0);
1165 $('#controlbox').slideDown(0);
1166 this.doLayout();
1167 }
1168 }
1169 });