Merge pull request #115 from M2Ys4U/irc_uri
[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.Channel({name: this.member.get('nick')});
52 panel.set('members', undefined);
53 kiwi.app.panels.add(panel);
54 panel.view.show();
55 },
56
57 infoClick: function (event) {
58 kiwi.app.controlbox.processInput('/whois ' + this.member.get('nick'));
59 },
60
61 slapClick: function (event) {
62 kiwi.app.controlbox.processInput('/slap ' + this.member.get('nick'));
63 }
64 });
65
66 kiwi.view.NickChangeBox = Backbone.View.extend({
67 events: {
68 'submit': 'changeNick',
69 'click .cancel': 'close'
70 },
71
72 initialize: function () {
73 this.$el = $($('#tmpl_nickchange').html());
74 },
75
76 render: function () {
77 // Add the UI component and give it focus
78 kiwi.app.controlbox.$el.prepend(this.$el);
79 this.$el.find('input').focus();
80
81 this.$el.css('bottom', kiwi.app.controlbox.$el.outerHeight(true));
82 },
83
84 close: function () {
85 this.$el.remove();
86
87 },
88
89 changeNick: function (event) {
90 var that = this;
91 kiwi.gateway.changeNick(this.$el.find('input').val(), function (err, val) {
92 that.close();
93 });
94 return false;
95 }
96 });
97
98 kiwi.view.ServerSelect = function () {
99 // Are currently showing all the controlls or just a nick_change box?
100 var state = 'all';
101
102 var model = Backbone.View.extend({
103 events: {
104 'submit form': 'submitForm',
105 'click .show_more': 'showMore'
106 },
107
108 initialize: function () {
109 this.$el = $($('#tmpl_server_select').html());
110
111 kiwi.gateway.bind('onconnect', this.networkConnected, this);
112 kiwi.gateway.bind('connecting', this.networkConnecting, this);
113
114 kiwi.gateway.bind('onirc_error', function (data) {
115 $('button', this.$el).attr('disabled', null);
116
117 if (data.error == 'nickname_in_use') {
118 this.setStatus('Nickname already taken');
119 this.show('nick_change');
120 }
121 }, this);
122 },
123
124 submitForm: function (event) {
125 if (state === 'nick_change') {
126 this.submitNickChange(event);
127 } else {
128 this.submitLogin(event);
129 }
130
131 $('button', this.$el).attr('disabled', 1);
132 return false;
133 },
134
135 submitLogin: function (event) {
136 // If submitting is disabled, don't do anything
137 if ($('button', this.$el).attr('disabled')) return;
138
139 var values = {
140 nick: $('.nick', this.$el).val(),
141 server: $('.server', this.$el).val(),
142 port: $('.port', this.$el).val(),
143 ssl: $('.ssl', this.$el).prop('checked'),
144 password: $('.password', this.$el).val(),
145 channel: $('.channel', this.$el).val(),
146 channel_key: $('.channel_key', this.$el).val()
147 };
148
149 this.trigger('server_connect', values);
150 },
151
152 submitNickChange: function (event) {
153 kiwi.gateway.changeNick($('.nick', this.$el).val());
154 this.networkConnecting();
155 },
156
157 showMore: function (event) {
158 $('.more', this.$el).slideDown('fast');
159 $('.server', this.$el).select();
160 },
161
162 populateFields: function (defaults) {
163 var nick, server, port, channel, channel_key, ssl, password;
164
165 defaults = defaults || {};
166
167 nick = defaults.nick || '';
168 server = defaults.server || '';
169 port = defaults.port || 6667;
170 ssl = defaults.ssl || 0;
171 password = defaults.password || '';
172 channel = defaults.channel || '';
173 channel_key = defaults.channel_key || '';
174
175 $('.nick', this.$el).val(nick);
176 $('.server', this.$el).val(server);
177 $('.port', this.$el).val(port);
178 $('.ssl', this.$el).prop('checked', ssl);
179 $('.password', this.$el).val(password);
180 $('.channel', this.$el).val(channel);
181 $('.channel_key', this.$el).val(channel_key);
182 },
183
184 hide: function () {
185 this.$el.slideUp();
186 },
187
188 show: function (new_state) {
189 new_state = new_state || 'all';
190
191 this.$el.show();
192
193 if (new_state === 'all') {
194 $('.show_more', this.$el).show();
195
196 } else if (new_state === 'more') {
197 $('.more', this.$el).slideDown('fast');
198
199 } else if (new_state === 'nick_change') {
200 $('.more', this.$el).hide();
201 $('.show_more', this.$el).hide();
202 }
203
204 state = new_state;
205 },
206
207 setStatus: function (text, class_name) {
208 $('.status', this.$el)
209 .text(text)
210 .attr('class', 'status')
211 .addClass(class_name)
212 .show();
213 },
214 clearStatus: function () {
215 $('.status', this.$el).hide();
216 },
217
218 networkConnected: function (event) {
219 this.setStatus('Connected :)', 'ok');
220 $('form', this.$el).hide();
221 },
222
223 networkConnecting: function (event) {
224 this.setStatus('Connecting..', 'ok');
225 },
226
227 showError: function (event) {
228 this.setStatus('Error connecting', 'error');
229 $('button', this.$el).attr('disabled', null);
230 this.show();
231 }
232 });
233
234
235 return new model(arguments);
236 };
237
238
239 kiwi.view.Panel = Backbone.View.extend({
240 tagName: "div",
241 className: "messages",
242 events: {
243 "click .chan": "chanClick"
244 },
245
246 initialize: function (options) {
247 this.initializePanel(options);
248 },
249
250 initializePanel: function (options) {
251 this.$el.css('display', 'none');
252 options = options || {};
253
254 // Containing element for this panel
255 if (options.container) {
256 this.$container = $(options.container);
257 } else {
258 this.$container = $('#panels .container1');
259 }
260
261 this.$el.appendTo(this.$container);
262
263 this.alert_level = 0;
264
265 this.model.bind('msg', this.newMsg, this);
266 this.msg_count = 0;
267
268 this.model.set({"view": this}, {"silent": true});
269 },
270
271 render: function () {
272 this.$el.empty();
273 this.model.get("backscroll").forEach(this.newMsg);
274 },
275 newMsg: function (msg) {
276 // TODO: make sure that the message pane is scrolled to the bottom (Or do we? ~Darren)
277 var re, line_msg, $this = this.$el,
278 nick_colour_hex;
279
280 // Escape any HTML that may be in here
281 msg.msg = $('<div />').text(msg.msg).html();
282
283 // Make the channels clickable
284 re = new RegExp('\\B([' + kiwi.gateway.get('channel_prefix') + '][^ ,.\\007]+)', 'g');
285 msg.msg = msg.msg.replace(re, function (match) {
286 return '<a class="chan">' + match + '</a>';
287 });
288
289
290 // Make links clickable
291 msg.msg = msg.msg.replace(/((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]*))?/gi, function (url) {
292 var nice;
293
294 // Add the http is no protoocol was found
295 if (url.match(/^www\./)) {
296 url = 'http://' + url;
297 }
298
299 nice = url;
300 if (nice.length > 100) {
301 nice = nice.substr(0, 100) + '...';
302 }
303
304 return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url + '">' + nice + '</a>';
305 });
306
307
308 // Convert IRC formatting into HTML formatting
309 msg.msg = formatIRCMsg(msg.msg);
310
311
312 // Add some colours to the nick (Method based on IRSSIs nickcolor.pl)
313 nick_colour_hex = (function (nick) {
314 var nick_int = 0, rgb;
315
316 _.map(nick.split(''), function (i) { nick_int += i.charCodeAt(0); });
317 rgb = hsl2rgb(nick_int % 255, 70, 35);
318 rgb = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);
319
320 return '#' + rgb.toString(16);
321 })(msg.nick);
322
323 msg.nick_style = 'color:' + nick_colour_hex + ';';
324
325 // Build up and add the line
326 line_msg = '<div class="msg <%= type %>"><div class="time"><%- time %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>';
327 $this.append(_.template(line_msg, msg));
328
329 // Activity/alerts based on the type of new message
330 if (msg.type.match(/^action /)) {
331 this.alert('action');
332 } else if (msg.msg.indexOf(kiwi.gateway.get('nick')) > -1) {
333 kiwi.app.view.alertWindow('* People are talking!');
334 this.alert('highlight');
335 } else {
336 // If this is the active panel, send an alert out
337 if (this.model.isActive()) {
338 kiwi.app.view.alertWindow('* People are talking!');
339 }
340 this.alert('activity');
341 }
342
343 this.scrollToBottom();
344
345 // Make sure our DOM isn't getting too large (Acts as scrollback)
346 this.msg_count++;
347 if (this.msg_count > 250) {
348 $('.msg:first', this.$el).remove();
349 this.msg_count--;
350 }
351 },
352 chanClick: function (event) {
353 if (event.target) {
354 kiwi.gateway.join($(event.target).text());
355 } else {
356 // IE...
357 kiwi.gateway.join($(event.srcElement).text());
358 }
359 },
360 show: function () {
361 var $this = this.$el;
362
363 // Hide all other panels and show this one
364 this.$container.children().css('display', 'none');
365 $this.css('display', 'block');
366
367 // Show this panels memberlist
368 var members = this.model.get("members");
369 if (members) {
370 $('#memberlists').show();
371 members.view.show();
372 } else {
373 // Memberlist not found for this panel, hide any active ones
374 $('#memberlists').hide().children().removeClass('active');
375 }
376
377 kiwi.app.view.doLayout();
378
379 this.scrollToBottom();
380 this.alert('none');
381
382 this.trigger('active', this.model);
383 kiwi.app.panels.trigger('active', this.model);
384 },
385
386
387 alert: function (level) {
388 // No need to highlight if this si the active panel
389 if (this.model == kiwi.app.panels.active) return;
390
391 var types, type_idx;
392 types = ['none', 'action', 'activity', 'highlight'];
393
394 // Default alert level
395 level = level || 'none';
396
397 // If this alert level does not exist, assume clearing current level
398 type_idx = _.indexOf(types, level);
399 if (!type_idx) {
400 level = 'none';
401 type_idx = 0;
402 }
403
404 // Only 'upgrade' the alert. Never down (unless clearing)
405 if (type_idx !== 0 && type_idx <= this.alert_level) {
406 return;
407 }
408
409 // Clear any existing levels
410 this.model.tab.removeClass(function (i, css) {
411 return (css.match(/\balert_\S+/g) || []).join(' ');
412 });
413
414 // Add the new level if there is one
415 if (level !== 'none') {
416 this.model.tab.addClass('alert_' + level);
417 }
418
419 this.alert_level = type_idx;
420 },
421
422
423 // Scroll to the bottom of the panel
424 scrollToBottom: function () {
425 // TODO: Don't scroll down if we're scrolled up the panel a little
426 this.$container[0].scrollTop = this.$container[0].scrollHeight;
427 }
428 });
429
430 kiwi.view.Applet = kiwi.view.Panel.extend({
431 className: 'applet',
432 initialize: function (options) {
433 this.initializePanel(options);
434 }
435 });
436
437 kiwi.view.Channel = kiwi.view.Panel.extend({
438 initialize: function (options) {
439 this.initializePanel(options);
440 this.model.bind('change:topic', this.topic, this);
441 },
442
443 topic: function (topic) {
444 if (typeof topic !== 'string' || !topic) {
445 topic = this.model.get("topic");
446 }
447
448 this.model.addMsg('', '== Topic for ' + this.model.get('name') + ' is: ' + topic, 'topic');
449
450 // If this is the active channel then update the topic bar
451 if (kiwi.app.panels.active === this) {
452 kiwi.app.topicbar.setCurrentTopic(this.model.get("topic"));
453 }
454 }
455 });
456
457 // Model for this = kiwi.model.PanelList
458 kiwi.view.Tabs = Backbone.View.extend({
459 events: {
460 'click li': 'tabClick',
461 'click li .part': 'partClick'
462 },
463
464 initialize: function () {
465 this.model.on("add", this.panelAdded, this);
466 this.model.on("remove", this.panelRemoved, this);
467 this.model.on("reset", this.render, this);
468
469 this.model.on('active', this.panelActive, this);
470
471 this.tabs_applets = $('ul.applets', this.$el);
472 this.tabs_msg = $('ul.channels', this.$el);
473
474 kiwi.gateway.on('change:name', function (gateway, new_val) {
475 $('span', this.model.server.tab).text(new_val);
476 }, this);
477 },
478 render: function () {
479 var that = this;
480
481 this.tabs_msg.empty();
482
483 // Add the server tab first
484 this.model.server.tab
485 .data('panel_id', this.model.server.cid)
486 .appendTo(this.tabs_msg);
487
488 // Go through each panel adding its tab
489 this.model.forEach(function (panel) {
490 // If this is the server panel, ignore as it's already added
491 if (panel == that.model.server) return;
492
493 panel.tab
494 .data('panel_id', panel.cid)
495 .appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg);
496 });
497
498 kiwi.app.view.doLayout();
499 },
500
501 updateTabTitle: function (panel, new_title) {
502 $('span', panel.tab).text(new_title);
503 },
504
505 panelAdded: function (panel) {
506 // Add a tab to the panel
507 panel.tab = $('<li><span>' + (panel.get('title') || panel.get('name')) + '</span></li>');
508
509 if (panel.isServer()) {
510 panel.tab.addClass('server');
511 }
512
513 panel.tab.data('panel_id', panel.cid)
514 .appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg);
515
516 panel.bind('change:title', this.updateTabTitle);
517 kiwi.app.view.doLayout();
518 },
519 panelRemoved: function (panel) {
520 panel.tab.remove();
521 delete panel.tab;
522
523 kiwi.app.view.doLayout();
524 },
525
526 panelActive: function (panel) {
527 // Remove any existing tabs or part images
528 $('.part', this.$el).remove();
529 this.tabs_applets.children().removeClass('active');
530 this.tabs_msg.children().removeClass('active');
531
532 panel.tab.addClass('active');
533
534 // Only show the part image on non-server tabs
535 if (!panel.isServer()) {
536 panel.tab.append('<span class="part"></span>');
537 }
538 },
539
540 tabClick: function (e) {
541 var tab = $(e.currentTarget);
542
543 var panel = this.model.getByCid(tab.data('panel_id'));
544 if (!panel) {
545 // A panel wasn't found for this tab... wadda fuck
546 return;
547 }
548
549 panel.view.show();
550 },
551
552 partClick: function (e) {
553 var tab = $(e.currentTarget).parent();
554 var panel = this.model.getByCid(tab.data('panel_id'));
555
556 // Only need to part if it's a channel
557 // If the nicklist is empty, we haven't joined the channel as yet
558 if (panel.isChannel() && panel.get('members').models.length > 0) {
559 kiwi.gateway.part(panel.get('name'));
560 } else {
561 panel.close();
562 }
563 },
564
565 next: function () {
566 var next = kiwi.app.panels.active.tab.next();
567 if (!next.length) next = $('li:first', this.tabs_msgs);
568
569 next.click();
570 },
571 prev: function () {
572 var prev = kiwi.app.panels.active.tab.prev();
573 if (!prev.length) prev = $('li:last', this.tabs_msgs);
574
575 prev.click();
576 }
577 });
578
579
580
581 kiwi.view.TopicBar = Backbone.View.extend({
582 events: {
583 'keydown div': 'process'
584 },
585
586 initialize: function () {
587 kiwi.app.panels.bind('active', function (active_panel) {
588 // If it's a channel topic, update and make editable
589 if (active_panel.isChannel()) {
590 this.setCurrentTopic(active_panel.get('topic') || '');
591 this.$el.find('div').attr('contentEditable', true);
592
593 } else {
594 // Not a channel topic.. clear and make uneditable
595 this.$el.find('div').attr('contentEditable', false)
596 .text('');
597 }
598 }, this);
599 },
600
601 process: function (ev) {
602 var inp = $(ev.currentTarget),
603 inp_val = inp.text();
604
605 // Only allow topic editing if this is a channel panel
606 if (!kiwi.app.panels.active.isChannel()) {
607 return false;
608 }
609
610 // If hit return key, update the current topic
611 if (ev.keyCode === 13) {
612 kiwi.gateway.topic(kiwi.app.panels.active.get('name'), inp_val);
613 return false;
614 }
615 },
616
617 setCurrentTopic: function (new_topic) {
618 new_topic = new_topic || '';
619
620 // We only want a plain text version
621 $('div', this.$el).html(formatIRCMsg(_.escape(new_topic)));
622 }
623 });
624
625
626
627 kiwi.view.ControlBox = Backbone.View.extend({
628 events: {
629 'keydown .inp': 'process',
630 'click .nick': 'showNickChange'
631 },
632
633 initialize: function () {
634 var that = this;
635
636 this.buffer = []; // Stores previously run commands
637 this.buffer_pos = 0; // The current position in the buffer
638
639 this.preprocessor = new InputPreProcessor();
640 this.preprocessor.recursive_depth = 5;
641
642 // Hold tab autocomplete data
643 this.tabcomplete = {active: false, data: [], prefix: ''};
644
645 kiwi.gateway.bind('change:nick', function () {
646 $('.nick', that.$el).text(this.get('nick'));
647 });
648 },
649
650 showNickChange: function (ev) {
651 (new kiwi.view.NickChangeBox()).render();
652 },
653
654 process: function (ev) {
655 var that = this,
656 inp = $(ev.currentTarget),
657 inp_val = inp.val(),
658 meta;
659
660 if (navigator.appVersion.indexOf("Mac") !== -1) {
661 meta = ev.ctrlKey;
662 } else {
663 meta = ev.altKey;
664 }
665
666 // If not a tab key, reset the tabcomplete data
667 if (this.tabcomplete.active && ev.keyCode !== 9) {
668 this.tabcomplete.active = false;
669 this.tabcomplete.data = [];
670 this.tabcomplete.prefix = '';
671 }
672
673 switch (true) {
674 case (ev.keyCode === 13): // return
675 inp_val = inp_val.trim();
676
677 if (inp_val) {
678 $.each(inp_val.split('\n'), function (idx, line) {
679 that.processInput(line);
680 });
681
682 this.buffer.push(inp_val);
683 this.buffer_pos = this.buffer.length;
684 }
685
686 inp.val('');
687 return false;
688
689 break;
690
691 case (ev.keyCode === 38): // up
692 if (this.buffer_pos > 0) {
693 this.buffer_pos--;
694 inp.val(this.buffer[this.buffer_pos]);
695 }
696 break;
697
698 case (ev.keyCode === 40): // down
699 if (this.buffer_pos < this.buffer.length) {
700 this.buffer_pos++;
701 inp.val(this.buffer[this.buffer_pos]);
702 }
703 break;
704
705 case (ev.keyCode === 37 && meta): // left
706 kiwi.app.panels.view.prev();
707 return false;
708
709 case (ev.keyCode === 39 && meta): // right
710 kiwi.app.panels.view.next();
711 return false;
712
713 case (ev.keyCode === 9): // tab
714 this.tabcomplete.active = true;
715 if (_.isEqual(this.tabcomplete.data, [])) {
716 // Get possible autocompletions
717 var ac_data = [];
718 $.each(kiwi.app.panels.active.get('members').models, function (i, member) {
719 if (!member) return;
720 ac_data.push(member.get('nick'));
721 });
722 ac_data = _.sortBy(ac_data, function (nick) {
723 return nick;
724 });
725 this.tabcomplete.data = ac_data;
726 }
727
728 if (inp_val[inp[0].selectionStart - 1] === ' ') {
729 return false;
730 }
731
732 (function () {
733 var tokens = inp_val.substring(0, inp[0].selectionStart).split(' '),
734 val,
735 p1,
736 newnick,
737 range,
738 nick = tokens[tokens.length - 1];
739 if (this.tabcomplete.prefix === '') {
740 this.tabcomplete.prefix = nick;
741 }
742
743 this.tabcomplete.data = _.select(this.tabcomplete.data, function (n) {
744 return (n.toLowerCase().indexOf(that.tabcomplete.prefix.toLowerCase()) === 0);
745 });
746
747 if (this.tabcomplete.data.length > 0) {
748 p1 = inp[0].selectionStart - (nick.length);
749 val = inp_val.substr(0, p1);
750 newnick = this.tabcomplete.data.shift();
751 this.tabcomplete.data.push(newnick);
752 val += newnick;
753 val += inp_val.substr(inp[0].selectionStart);
754 inp.val(val);
755
756 if (inp[0].setSelectionRange) {
757 inp[0].setSelectionRange(p1 + newnick.length, p1 + newnick.length);
758 } else if (inp[0].createTextRange) { // not sure if this bit is actually needed....
759 range = inp[0].createTextRange();
760 range.collapse(true);
761 range.moveEnd('character', p1 + newnick.length);
762 range.moveStart('character', p1 + newnick.length);
763 range.select();
764 }
765 }
766 }).apply(this);
767 return false;
768 }
769 },
770
771
772 processInput: function (command_raw) {
773 var command, params,
774 pre_processed;
775
776 // The default command
777 if (command_raw[0] !== '/') {
778 command_raw = '/msg ' + kiwi.app.panels.active.get('name') + ' ' + command_raw;
779 }
780
781 // Process the raw command for any aliases
782 this.preprocessor.vars.server = kiwi.gateway.get('name');
783 this.preprocessor.vars.channel = kiwi.app.panels.active.get('name');
784 this.preprocessor.vars.destination = this.preprocessor.vars.channel;
785 command_raw = this.preprocessor.process(command_raw);
786
787 // Extract the command and parameters
788 params = command_raw.split(' ');
789 if (params[0][0] === '/') {
790 command = params[0].substr(1).toLowerCase();
791 params = params.splice(1, params.length - 1);
792 } else {
793 // Default command
794 command = 'msg';
795 params.unshift(kiwi.app.panels.active.get('name'));
796 }
797
798 // Trigger the command events
799 this.trigger('command', {command: command, params: params});
800 this.trigger('command_' + command, {command: command, params: params});
801
802 // If we didn't have any listeners for this event, fire a special case
803 // TODO: This feels dirty. Should this really be done..?
804 if (!this._callbacks['command_' + command]) {
805 this.trigger('unknown_command', {command: command, params: params});
806 }
807 }
808 });
809
810
811
812
813 kiwi.view.StatusMessage = Backbone.View.extend({
814 initialize: function () {
815 this.$el.hide();
816
817 // Timer for hiding the message after X seconds
818 this.tmr = null;
819 },
820
821 text: function (text, opt) {
822 // Defaults
823 opt = opt || {};
824 opt.type = opt.type || '';
825 opt.timeout = opt.timeout || 5000;
826
827 this.$el.text(text).attr('class', opt.type);
828 this.$el.slideDown(kiwi.app.view.doLayout);
829
830 if (opt.timeout) this.doTimeout(opt.timeout);
831 },
832
833 html: function (html, opt) {
834 // Defaults
835 opt = opt || {};
836 opt.type = opt.type || '';
837 opt.timeout = opt.timeout || 5000;
838
839 this.$el.html(text).attr('class', opt.type);
840 this.$el.slideDown(kiwi.app.view.doLayout);
841
842 if (opt.timeout) this.doTimeout(opt.timeout);
843 },
844
845 hide: function () {
846 this.$el.slideUp(kiwi.app.view.doLayout);
847 },
848
849 doTimeout: function (length) {
850 if (this.tmr) clearTimeout(this.tmr);
851 var that = this;
852 this.tmr = setTimeout(function () { that.hide(); }, length);
853 }
854 });
855
856
857
858
859 kiwi.view.ResizeHandler = Backbone.View.extend({
860 events: {
861 'mousedown': 'startDrag',
862 'mouseup': 'stopDrag'
863 },
864
865 initialize: function () {
866 this.dragging = false;
867 this.starting_width = {};
868
869 $(window).on('mousemove', $.proxy(this.onDrag, this));
870 },
871
872 startDrag: function (event) {
873 this.dragging = true;
874 },
875
876 stopDrag: function (event) {
877 this.dragging = false;
878 },
879
880 onDrag: function (event) {
881 if (!this.dragging) return;
882
883 this.$el.css('left', event.clientX - (this.$el.outerWidth(true) / 2));
884 $('#memberlists').css('width', this.$el.parent().width() - (this.$el.position().left + this.$el.outerWidth()));
885 kiwi.app.view.doLayout();
886 }
887 });
888
889
890
891 kiwi.view.AppToolbar = Backbone.View.extend({
892 events: {
893 'click .settings': 'clickSettings'
894 },
895
896 initialize: function () {
897 },
898
899 clickSettings: function (event) {
900 kiwi.app.controlbox.processInput('/settings');
901 }
902 });
903
904
905
906 kiwi.view.Application = Backbone.View.extend({
907 initialize: function () {
908 $(window).resize(this.doLayout);
909 $('#toolbar').resize(this.doLayout);
910 $('#controlbox').resize(this.doLayout);
911
912 this.doLayout();
913
914 $(document).keydown(this.setKeyFocus);
915
916 // Confirmation require to leave the page
917 window.onbeforeunload = function () {
918 if (kiwi.gateway.isConnected()) {
919 return 'This will close all KiwiIRC conversations. Are you sure you want to close this window?';
920 }
921 };
922 },
923
924
925 // Globally shift focus to the command input box on a keypress
926 setKeyFocus: function (ev) {
927 // If we're copying text, don't shift focus
928 if (ev.ctrlKey || ev.altKey || ev.metaKey) {
929 return;
930 }
931
932 // If we're typing into an input box somewhere, ignore
933 if ((ev.target.tagName.toLowerCase() === 'input') || $(ev.target).attr('contenteditable')) {
934 return;
935 }
936
937 $('#controlbox .inp').focus();
938 },
939
940
941 doLayout: function () {
942 var el_panels = $('#panels');
943 var el_memberlists = $('#memberlists');
944 var el_toolbar = $('#toolbar');
945 var el_controlbox = $('#controlbox');
946 var el_resize_handle = $('#memberlists_resize_handle');
947
948 var css_heights = {
949 top: el_toolbar.outerHeight(true),
950 bottom: el_controlbox.outerHeight(true)
951 };
952
953
954 // If any elements are not visible, full size the panals instead
955 if (!el_toolbar.is(':visible')) {
956 css_heights.top = 0;
957 }
958
959 if (!el_controlbox.is(':visible')) {
960 css_heights.bottom = 0;
961 }
962
963 // Apply the CSS sizes
964 el_panels.css(css_heights);
965 el_memberlists.css(css_heights);
966 el_resize_handle.css(css_heights);
967
968 // Set the panels width depending on the memberlist visibility
969 if (el_memberlists.css('display') != 'none') {
970 // Handle + panels to the side of the memberlist
971 el_panels.css('right', el_memberlists.outerWidth(true) + el_resize_handle.outerWidth(true));
972 el_resize_handle.css('left', el_memberlists.position().left - el_resize_handle.outerWidth(true));
973 } else {
974 // Memberlist is hidden so handle + panels to the right edge
975 el_panels.css('right', el_resize_handle.outerWidth(true));
976 el_resize_handle.css('left', el_panels.outerWidth(true));
977 }
978 },
979
980
981 alertWindow: function (title) {
982 if (!this.alertWindowTimer) {
983 this.alertWindowTimer = new (function () {
984 var that = this;
985 var tmr;
986 var has_focus = true;
987 var state = 0;
988 var default_title = 'Kiwi IRC';
989 var title = 'Kiwi IRC';
990
991 this.setTitle = function (new_title) {
992 new_title = new_title || default_title;
993 window.document.title = new_title;
994 return new_title;
995 };
996
997 this.start = function (new_title) {
998 // Don't alert if we already have focus
999 if (has_focus) return;
1000
1001 title = new_title;
1002 if (tmr) return;
1003 tmr = setInterval(this.update, 1000);
1004 };
1005
1006 this.stop = function () {
1007 // Stop the timer and clear the title
1008 if (tmr) clearInterval(tmr);
1009 tmr = null;
1010 this.setTitle();
1011
1012 // Some browsers don't always update the last title correctly
1013 // Wait a few seconds and then reset
1014 setTimeout(this.reset, 2000);
1015 };
1016
1017 this.reset = function () {
1018 if (tmr) return;
1019 that.setTitle();
1020 };
1021
1022
1023 this.update = function () {
1024 if (state === 0) {
1025 that.setTitle(title);
1026 state = 1;
1027 } else {
1028 that.setTitle();
1029 state = 0;
1030 }
1031 };
1032
1033 $(window).focus(function (event) {
1034 has_focus = true;
1035 that.stop();
1036
1037 // Some browsers don't always update the last title correctly
1038 // Wait a few seconds and then reset
1039 setTimeout(that.reset, 2000);
1040 });
1041
1042 $(window).blur(function (event) {
1043 has_focus = false;
1044 });
1045 })();
1046 }
1047
1048 this.alertWindowTimer.start(title);
1049 },
1050
1051
1052 barsHide: function (instant) {
1053 var that = this;
1054
1055 if (!instant) {
1056 $('#toolbar').slideUp({queue: false, duration: 400, step: this.doLayout});
1057 $('#controlbox').slideUp({queue: false, duration: 400, step: this.doLayout});
1058 } else {
1059 $('#toolbar').slideUp(0);
1060 $('#controlbox').slideUp(0);
1061 this.doLayout();
1062 }
1063 },
1064
1065 barsShow: function (instant) {
1066 var that = this;
1067
1068 if (!instant) {
1069 $('#toolbar').slideDown({queue: false, duration: 400, step: this.doLayout});
1070 $('#controlbox').slideDown({queue: false, duration: 400, step: this.doLayout});
1071 } else {
1072 $('#toolbar').slideDown(0);
1073 $('#controlbox').slideDown(0);
1074 this.doLayout();
1075 }
1076 }
1077 });