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