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