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 */
4 _kiwi
.view
.MemberList
= Backbone
.View
.extend({
7 "click .nick": "nickClick"
9 initialize: function (options
) {
10 this.model
.bind('all', this.render
, this);
11 $(this.el
).appendTo('#memberlists');
14 var $this = $(this.el
);
16 this.model
.forEach(function (member
) {
17 $('<li><a class="nick"><span class="prefix">' + member
.get("prefix") + '</span>' + member
.get("nick") + '</a></li>')
19 .data('member', member
);
22 nickClick: function (x
) {
23 var target
= $(x
.currentTarget
).parent('li'),
24 member
= target
.data('member'),
25 userbox
= new _kiwi
.view
.UserBox();
27 userbox
.member
= member
;
28 $('.userbox', this.$el
).remove();
29 target
.append(userbox
.$el
);
32 $('#memberlists').children().removeClass('active');
33 $(this.el
).addClass('active');
39 _kiwi
.view
.UserBox
= Backbone
.View
.extend({
41 'click .query': 'queryClick',
42 'click .info': 'infoClick',
43 'click .slap': 'slapClick'
46 initialize: function () {
47 this.$el
= $($('#tmpl_userbox').html());
50 queryClick: function (event
) {
51 var panel
= new _kiwi
.model
.Query({name
: this.member
.get('nick')});
52 _kiwi
.app
.panels
.add(panel
);
56 infoClick: function (event
) {
57 _kiwi
.app
.controlbox
.processInput('/whois ' + this.member
.get('nick'));
60 slapClick: function (event
) {
61 _kiwi
.app
.controlbox
.processInput('/slap ' + this.member
.get('nick'));
65 _kiwi
.view
.NickChangeBox
= Backbone
.View
.extend({
67 'submit': 'changeNick',
68 'click .cancel': 'close'
71 initialize: function () {
72 this.$el
= $($('#tmpl_nickchange').html());
76 // Add the UI component and give it focus
77 _kiwi
.app
.controlbox
.$el
.prepend(this.$el
);
78 this.$el
.find('input').focus();
80 this.$el
.css('bottom', _kiwi
.app
.controlbox
.$el
.outerHeight(true));
88 changeNick: function (event
) {
90 _kiwi
.gateway
.changeNick(this.$el
.find('input').val(), function (err
, val
) {
97 _kiwi
.view
.ServerSelect = function () {
98 // Are currently showing all the controlls or just a nick_change box?
101 var model
= Backbone
.View
.extend({
103 'submit form': 'submitForm',
104 'click .show_more': 'showMore'
107 initialize: function () {
108 this.$el
= $($('#tmpl_server_select').html());
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');
119 _kiwi
.gateway
.bind('onconnect', this.networkConnected
, this);
120 _kiwi
.gateway
.bind('connecting', this.networkConnecting
, this);
122 _kiwi
.gateway
.bind('onirc_error', function (data
) {
123 $('button', this.$el
).attr('disabled', null);
125 if (data
.error
== 'nickname_in_use') {
126 this.setStatus('Nickname already taken');
127 this.show('nick_change');
132 submitForm: function (event
) {
133 if (state
=== 'nick_change') {
134 this.submitNickChange(event
);
136 this.submitLogin(event
);
139 $('button', this.$el
).attr('disabled', 1);
143 submitLogin: function (event
) {
144 // If submitting is disabled, don't do anything
145 if ($('button', this.$el
).attr('disabled')) return;
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()
157 this.trigger('server_connect', values
);
160 submitNickChange: function (event
) {
161 _kiwi
.gateway
.changeNick($('.nick', this.$el
).val());
162 this.networkConnecting();
165 showMore: function (event
) {
166 $('.more', this.$el
).slideDown('fast');
167 $('.server', this.$el
).select();
170 populateFields: function (defaults
) {
171 var nick
, server
, port
, channel
, channel_key
, ssl
, password
;
173 defaults
= defaults
|| {};
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
|| '';
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
);
196 show: function (new_state
) {
197 new_state
= new_state
|| 'all';
201 if (new_state
=== 'all') {
202 $('.show_more', this.$el
).show();
204 } else if (new_state
=== 'more') {
205 $('.more', this.$el
).slideDown('fast');
207 } else if (new_state
=== 'nick_change') {
208 $('.more', this.$el
).hide();
209 $('.show_more', this.$el
).hide();
215 setStatus: function (text
, class_name
) {
216 $('.status', this.$el
)
218 .attr('class', 'status')
219 .addClass(class_name
)
222 clearStatus: function () {
223 $('.status', this.$el
).hide();
226 networkConnected: function (event
) {
227 this.setStatus('Connected :)', 'ok');
228 $('form', this.$el
).hide();
231 networkConnecting: function (event
) {
232 this.setStatus('Connecting..', 'ok');
235 showError: function (event
) {
236 this.setStatus('Error connecting', 'error');
237 $('button', this.$el
).attr('disabled', null);
243 return new model(arguments
);
247 _kiwi
.view
.Panel
= Backbone
.View
.extend({
249 className
: "messages",
251 "click .chan": "chanClick",
252 'click .media .open': 'mediaClick',
253 'mouseenter .msg .nick': 'msgEnter',
254 'mouseleave .msg .nick': 'msgLeave'
257 initialize: function (options
) {
258 this.initializePanel(options
);
261 initializePanel: function (options
) {
262 this.$el
.css('display', 'none');
263 options
= options
|| {};
265 // Containing element for this panel
266 if (options
.container
) {
267 this.$container
= $(options
.container
);
269 this.$container
= $('#panels .container1');
272 this.$el
.appendTo(this.$container
);
274 this.alert_level
= 0;
276 this.model
.bind('msg', this.newMsg
, this);
279 this.model
.set({"view": this}, {"silent": true});
282 render: function () {
284 this.model
.get("backscroll").forEach(this.newMsg
);
286 newMsg: function (msg
) {
287 // TODO: make sure that the message pane is scrolled to the bottom (Or do we? ~Darren)
288 var re
, line_msg
, $this = this.$el
,
289 nick_colour_hex
, nick_hex
, is_highlight
, msg_css_classes
= '';
291 // Nick highlight detecting
292 if ((new RegExp('\\b' + _kiwi
.gateway
.get('nick') + '\\b', 'i')).test(msg
.msg
)) {
294 msg_css_classes
+= ' highlight';
297 // Escape any HTML that may be in here
298 msg
.msg
= $('<div />').text(msg
.msg
).html();
300 // Make the channels clickable
301 re
= new RegExp('(?:^|\\s)([' + _kiwi
.gateway
.get('channel_prefix') + '][^ ,.\\007]+)', 'g');
302 msg
.msg
= msg
.msg
.replace(re
, function (match
) {
303 return '<a class="chan" data-channel="' + match
.trim() + '">' + match
+ '</a>';
307 // Parse any links found
308 msg
.msg
= msg
.msg
.replace(/(([A-Za-z0-9\-]+\:\/\/)|(www\.))([\w.]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w#!:.?$'()[\]*,;~+=&%@!\-\/]*)?/gi, function (url
) {
309 var nice
, extra_html
= '';
311 // Add the http if no protoocol was found
312 if (url
.match(/^www\./)) {
313 url
= 'http://' + url
;
316 // Shorten the displayed URL if it's going to be too long
318 if (nice
.length
> 100) {
319 nice
= nice
.substr(0, 100) + '...';
322 // Get any media HTML if supported
323 extra_html
= _kiwi
.view
.MediaMessage
.buildHtml(url
);
325 // Make the link clickable
326 return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url
+ '">' + nice
+ '</a> ' + extra_html
;
330 // Convert IRC formatting into HTML formatting
331 msg
.msg
= formatIRCMsg(msg
.msg
);
334 // Add some colours to the nick (Method based on IRSSIs nickcolor.pl)
335 nick_colour_hex
= (function (nick
) {
336 var nick_int
= 0, rgb
;
338 _
.map(nick
.split(''), function (i
) { nick_int
+= i
.charCodeAt(0); });
339 rgb
= hsl2rgb(nick_int
% 255, 70, 35);
340 rgb
= rgb
[2] | (rgb
[1] << 8) | (rgb
[0] << 16);
342 return '#' + rgb
.toString(16);
345 msg
.nick_style
= 'color:' + nick_colour_hex
+ ';';
347 // Generate a hex string from the nick to be used as a CSS class name
348 nick_hex
= msg
.nick_css_class
= '';
350 _
.map(msg
.nick
.split(''), function (char) {
351 nick_hex
+= char.charCodeAt(0).toString(16);
353 msg_css_classes
+= ' nick_' + nick_hex
;
356 // Build up and add the line
357 msg
.msg_css_classes
= msg_css_classes
;
358 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>';
359 $this.append(_
.template(line_msg
, msg
));
361 // Activity/alerts based on the type of new message
362 if (msg
.type
.match(/^action /)) {
363 this.alert('action');
364 } else if (is_highlight
) {
365 _kiwi
.app
.view
.alertWindow('* People are talking!');
366 this.alert('highlight');
368 // If this is the active panel, send an alert out
369 if (this.model
.isActive()) {
370 _kiwi
.app
.view
.alertWindow('* People are talking!');
372 this.alert('activity');
375 this.scrollToBottom();
377 // Make sure our DOM isn't getting too large (Acts as scrollback)
379 if (this.msg_count
> (parseInt(_kiwi
.global
.settings
.get('scrollback'), 10) || 250)) {
380 $('.msg:first', this.$el
).remove();
384 chanClick: function (event
) {
386 _kiwi
.gateway
.join($(event
.target
).data('channel'));
389 _kiwi
.gateway
.join($(event
.srcElement
).data('channel'));
393 mediaClick: function (event
) {
394 var $media
= $(event
.target
).parents('.media');
397 if ($media
.data('media')) {
398 media_message
= $media
.data('media');
400 media_message
= new _kiwi
.view
.MediaMessage({el
: $media
[0]});
401 $media
.data('media', media_message
);
404 $media
.data('media', media_message
);
406 media_message
.open();
409 msgEnter: function (event
) {
412 // Find a valid class that this element has
413 _
.each($(event
.currentTarget
).parent('.msg').attr('class').split(' '), function (css_class
) {
414 if (css_class
.match(/^nick_[a-z0-9]+/i)) {
415 nick_class
= css_class
;
419 // If no class was found..
420 if (!nick_class
) return;
422 $('.'+nick_class
).addClass('global_nick_highlight');
425 msgLeave: function (event
) {
428 // Find a valid class that this element has
429 _
.each($(event
.currentTarget
).parent('.msg').attr('class').split(' '), function (css_class
) {
430 if (css_class
.match(/^nick_[a-z0-9]+/i)) {
431 nick_class
= css_class
;
435 // If no class was found..
436 if (!nick_class
) return;
438 $('.'+nick_class
).removeClass('global_nick_highlight');
442 var $this = this.$el
;
444 // Hide all other panels and show this one
445 this.$container
.children().css('display', 'none');
446 $this.css('display', 'block');
448 // Show this panels memberlist
449 var members
= this.model
.get("members");
451 $('#memberlists').show();
454 // Memberlist not found for this panel, hide any active ones
455 $('#memberlists').hide().children().removeClass('active');
458 _kiwi
.app
.view
.doLayout();
461 this.trigger('active', this.model
);
462 _kiwi
.app
.panels
.trigger('active', this.model
);
464 this.scrollToBottom(true);
468 alert: function (level
) {
469 // No need to highlight if this si the active panel
470 if (this.model
== _kiwi
.app
.panels
.active
) return;
473 types
= ['none', 'action', 'activity', 'highlight'];
475 // Default alert level
476 level
= level
|| 'none';
478 // If this alert level does not exist, assume clearing current level
479 type_idx
= _
.indexOf(types
, level
);
485 // Only 'upgrade' the alert. Never down (unless clearing)
486 if (type_idx
!== 0 && type_idx
<= this.alert_level
) {
490 // Clear any existing levels
491 this.model
.tab
.removeClass(function (i
, css
) {
492 return (css
.match(/\balert_\S+/g) || []).join(' ');
495 // Add the new level if there is one
496 if (level
!== 'none') {
497 this.model
.tab
.addClass('alert_' + level
);
500 this.alert_level
= type_idx
;
504 // Scroll to the bottom of the panel
505 scrollToBottom: function (force_down
) {
506 // If this isn't the active panel, don't scroll
507 if (this.model
!== _kiwi
.app
.panels
.active
) return;
509 // Don't scroll down if we're scrolled up the panel a little
510 if (force_down
|| this.$container
.scrollTop() + this.$container
.height() > this.$el
.outerHeight() - 150) {
511 this.$container
[0].scrollTop
= this.$container
[0].scrollHeight
;
516 _kiwi
.view
.Applet
= _kiwi
.view
.Panel
.extend({
518 initialize: function (options
) {
519 this.initializePanel(options
);
523 _kiwi
.view
.Channel
= _kiwi
.view
.Panel
.extend({
524 initialize: function (options
) {
525 this.initializePanel(options
);
526 this.model
.bind('change:topic', this.topic
, this);
529 topic: function (topic
) {
530 if (typeof topic
!== 'string' || !topic
) {
531 topic
= this.model
.get("topic");
534 this.model
.addMsg('', '== Topic for ' + this.model
.get('name') + ' is: ' + topic
, 'topic');
536 // If this is the active channel then update the topic bar
537 if (_kiwi
.app
.panels
.active
=== this) {
538 _kiwi
.app
.topicbar
.setCurrentTopic(this.model
.get("topic"));
543 // Model for this = _kiwi.model.PanelList
544 _kiwi
.view
.Tabs
= Backbone
.View
.extend({
546 'click li': 'tabClick',
547 'click li .part': 'partClick'
550 initialize: function () {
551 this.model
.on("add", this.panelAdded
, this);
552 this.model
.on("remove", this.panelRemoved
, this);
553 this.model
.on("reset", this.render
, this);
555 this.model
.on('active', this.panelActive
, this);
557 this.tabs_applets
= $('ul.applets', this.$el
);
558 this.tabs_msg
= $('ul.channels', this.$el
);
560 _kiwi
.gateway
.on('change:name', function (gateway
, new_val
) {
561 $('span', this.model
.server
.tab
).text(new_val
);
564 render: function () {
567 this.tabs_msg
.empty();
569 // Add the server tab first
570 this.model
.server
.tab
571 .data('panel_id', this.model
.server
.cid
)
572 .appendTo(this.tabs_msg
);
574 // Go through each panel adding its tab
575 this.model
.forEach(function (panel
) {
576 // If this is the server panel, ignore as it's already added
577 if (panel
== that
.model
.server
) return;
580 .data('panel_id', panel
.cid
)
581 .appendTo(panel
.isApplet() ? this.tabs_applets
: this.tabs_msg
);
584 _kiwi
.app
.view
.doLayout();
587 updateTabTitle: function (panel
, new_title
) {
588 $('span', panel
.tab
).text(new_title
);
591 panelAdded: function (panel
) {
592 // Add a tab to the panel
593 panel
.tab
= $('<li><span>' + (panel
.get('title') || panel
.get('name')) + '</span></li>');
595 if (panel
.isServer()) {
596 panel
.tab
.addClass('server');
599 panel
.tab
.data('panel_id', panel
.cid
)
600 .appendTo(panel
.isApplet() ? this.tabs_applets
: this.tabs_msg
);
602 panel
.bind('change:title', this.updateTabTitle
);
603 _kiwi
.app
.view
.doLayout();
605 panelRemoved: function (panel
) {
609 _kiwi
.app
.view
.doLayout();
612 panelActive: function (panel
) {
613 // Remove any existing tabs or part images
614 $('.part', this.$el
).remove();
615 this.tabs_applets
.children().removeClass('active');
616 this.tabs_msg
.children().removeClass('active');
618 panel
.tab
.addClass('active');
620 // Only show the part image on non-server tabs
621 if (!panel
.isServer()) {
622 panel
.tab
.append('<span class="part"></span>');
626 tabClick: function (e
) {
627 var tab
= $(e
.currentTarget
);
629 var panel
= this.model
.getByCid(tab
.data('panel_id'));
631 // A panel wasn't found for this tab... wadda fuck
638 partClick: function (e
) {
639 var tab
= $(e
.currentTarget
).parent();
640 var panel
= this.model
.getByCid(tab
.data('panel_id'));
642 // Only need to part if it's a channel
643 // If the nicklist is empty, we haven't joined the channel as yet
644 if (panel
.isChannel() && panel
.get('members').models
.length
> 0) {
645 _kiwi
.gateway
.part(panel
.get('name'));
652 var next
= _kiwi
.app
.panels
.active
.tab
.next();
653 if (!next
.length
) next
= $('li:first', this.tabs_msgs
);
658 var prev
= _kiwi
.app
.panels
.active
.tab
.prev();
659 if (!prev
.length
) prev
= $('li:last', this.tabs_msgs
);
667 _kiwi
.view
.TopicBar
= Backbone
.View
.extend({
669 'keydown div': 'process'
672 initialize: function () {
673 _kiwi
.app
.panels
.bind('active', function (active_panel
) {
674 // If it's a channel topic, update and make editable
675 if (active_panel
.isChannel()) {
676 this.setCurrentTopic(active_panel
.get('topic') || '');
677 this.$el
.find('div').attr('contentEditable', true);
680 // Not a channel topic.. clear and make uneditable
681 this.$el
.find('div').attr('contentEditable', false)
687 process: function (ev
) {
688 var inp
= $(ev
.currentTarget
),
689 inp_val
= inp
.text();
691 // Only allow topic editing if this is a channel panel
692 if (!_kiwi
.app
.panels
.active
.isChannel()) {
696 // If hit return key, update the current topic
697 if (ev
.keyCode
=== 13) {
698 _kiwi
.gateway
.topic(_kiwi
.app
.panels
.active
.get('name'), inp_val
);
703 setCurrentTopic: function (new_topic
) {
704 new_topic
= new_topic
|| '';
706 // We only want a plain text version
707 $('div', this.$el
).html(formatIRCMsg(_
.escape(new_topic
)));
713 _kiwi
.view
.ControlBox
= Backbone
.View
.extend({
715 'keydown .inp': 'process',
716 'click .nick': 'showNickChange'
719 initialize: function () {
722 this.buffer
= []; // Stores previously run commands
723 this.buffer_pos
= 0; // The current position in the buffer
725 this.preprocessor
= new InputPreProcessor();
726 this.preprocessor
.recursive_depth
= 5;
728 // Hold tab autocomplete data
729 this.tabcomplete
= {active
: false, data
: [], prefix
: ''};
731 _kiwi
.gateway
.bind('change:nick', function () {
732 $('.nick', that
.$el
).text(this.get('nick'));
736 showNickChange: function (ev
) {
737 (new _kiwi
.view
.NickChangeBox()).render();
740 process: function (ev
) {
742 inp
= $(ev
.currentTarget
),
746 if (navigator
.appVersion
.indexOf("Mac") !== -1) {
752 // If not a tab key, reset the tabcomplete data
753 if (this.tabcomplete
.active
&& ev
.keyCode
!== 9) {
754 this.tabcomplete
.active
= false;
755 this.tabcomplete
.data
= [];
756 this.tabcomplete
.prefix
= '';
760 case (ev
.keyCode
=== 13): // return
761 inp_val
= inp_val
.trim();
764 $.each(inp_val
.split('\n'), function (idx
, line
) {
765 that
.processInput(line
);
768 this.buffer
.push(inp_val
);
769 this.buffer_pos
= this.buffer
.length
;
777 case (ev
.keyCode
=== 38): // up
778 if (this.buffer_pos
> 0) {
780 inp
.val(this.buffer
[this.buffer_pos
]);
784 case (ev
.keyCode
=== 40): // down
785 if (this.buffer_pos
< this.buffer
.length
) {
787 inp
.val(this.buffer
[this.buffer_pos
]);
791 case (ev
.keyCode
=== 37 && meta
): // left
792 _kiwi
.app
.panels
.view
.prev();
795 case (ev
.keyCode
=== 39 && meta
): // right
796 _kiwi
.app
.panels
.view
.next();
799 case (ev
.keyCode
=== 9): // tab
800 this.tabcomplete
.active
= true;
801 if (_
.isEqual(this.tabcomplete
.data
, [])) {
802 // Get possible autocompletions
804 $.each(_kiwi
.app
.panels
.active
.get('members').models
, function (i
, member
) {
806 ac_data
.push(member
.get('nick'));
808 ac_data
= _
.sortBy(ac_data
, function (nick
) {
811 this.tabcomplete
.data
= ac_data
;
814 if (inp_val
[inp
[0].selectionStart
- 1] === ' ') {
819 var tokens
= inp_val
.substring(0, inp
[0].selectionStart
).split(' '),
824 nick
= tokens
[tokens
.length
- 1];
825 if (this.tabcomplete
.prefix
=== '') {
826 this.tabcomplete
.prefix
= nick
;
829 this.tabcomplete
.data
= _
.select(this.tabcomplete
.data
, function (n
) {
830 return (n
.toLowerCase().indexOf(that
.tabcomplete
.prefix
.toLowerCase()) === 0);
833 if (this.tabcomplete
.data
.length
> 0) {
834 p1
= inp
[0].selectionStart
- (nick
.length
);
835 val
= inp_val
.substr(0, p1
);
836 newnick
= this.tabcomplete
.data
.shift();
837 this.tabcomplete
.data
.push(newnick
);
839 val
+= inp_val
.substr(inp
[0].selectionStart
);
842 if (inp
[0].setSelectionRange
) {
843 inp
[0].setSelectionRange(p1
+ newnick
.length
, p1
+ newnick
.length
);
844 } else if (inp
[0].createTextRange
) { // not sure if this bit is actually needed....
845 range
= inp
[0].createTextRange();
846 range
.collapse(true);
847 range
.moveEnd('character', p1
+ newnick
.length
);
848 range
.moveStart('character', p1
+ newnick
.length
);
858 processInput: function (command_raw
) {
862 // The default command
863 if (command_raw
[0] !== '/' || command_raw
.substr(0, 2) === '//') {
864 // Remove any slash escaping at the start (ie. //)
865 command_raw
= command_raw
.replace(/^\/\//, '/');
867 // Prepend the default command
868 command_raw
= '/msg ' + _kiwi
.app
.panels
.active
.get('name') + ' ' + command_raw
;
871 // Process the raw command for any aliases
872 this.preprocessor
.vars
.server
= _kiwi
.gateway
.get('name');
873 this.preprocessor
.vars
.channel
= _kiwi
.app
.panels
.active
.get('name');
874 this.preprocessor
.vars
.destination
= this.preprocessor
.vars
.channel
;
875 command_raw
= this.preprocessor
.process(command_raw
);
877 // Extract the command and parameters
878 params
= command_raw
.split(' ');
879 if (params
[0][0] === '/') {
880 command
= params
[0].substr(1).toLowerCase();
881 params
= params
.splice(1, params
.length
- 1);
885 params
.unshift(_kiwi
.app
.panels
.active
.get('name'));
888 // Trigger the command events
889 this.trigger('command', {command
: command
, params
: params
});
890 this.trigger('command_' + command
, {command
: command
, params
: params
});
892 // If we didn't have any listeners for this event, fire a special case
893 // TODO: This feels dirty. Should this really be done..?
894 if (!this._callbacks
['command_' + command
]) {
895 this.trigger('unknown_command', {command
: command
, params
: params
});
903 _kiwi
.view
.StatusMessage
= Backbone
.View
.extend({
904 initialize: function () {
907 // Timer for hiding the message after X seconds
911 text: function (text
, opt
) {
914 opt
.type
= opt
.type
|| '';
915 opt
.timeout
= opt
.timeout
|| 5000;
917 this.$el
.text(text
).attr('class', opt
.type
);
918 this.$el
.slideDown(_kiwi
.app
.view
.doLayout
);
920 if (opt
.timeout
) this.doTimeout(opt
.timeout
);
923 html: function (html
, opt
) {
926 opt
.type
= opt
.type
|| '';
927 opt
.timeout
= opt
.timeout
|| 5000;
929 this.$el
.html(text
).attr('class', opt
.type
);
930 this.$el
.slideDown(_kiwi
.app
.view
.doLayout
);
932 if (opt
.timeout
) this.doTimeout(opt
.timeout
);
936 this.$el
.slideUp(_kiwi
.app
.view
.doLayout
);
939 doTimeout: function (length
) {
940 if (this.tmr
) clearTimeout(this.tmr
);
942 this.tmr
= setTimeout(function () { that
.hide(); }, length
);
949 _kiwi
.view
.ResizeHandler
= Backbone
.View
.extend({
951 'mousedown': 'startDrag',
952 'mouseup': 'stopDrag'
955 initialize: function () {
956 this.dragging
= false;
957 this.starting_width
= {};
959 $(window
).on('mousemove', $.proxy(this.onDrag
, this));
962 startDrag: function (event
) {
963 this.dragging
= true;
966 stopDrag: function (event
) {
967 this.dragging
= false;
970 onDrag: function (event
) {
971 if (!this.dragging
) return;
973 this.$el
.css('left', event
.clientX
- (this.$el
.outerWidth(true) / 2));
974 $('#memberlists').css('width', this.$el
.parent().width() - (this.$el
.position().left
+ this.$el
.outerWidth()));
975 _kiwi
.app
.view
.doLayout();
981 _kiwi
.view
.AppToolbar
= Backbone
.View
.extend({
983 'click .settings': 'clickSettings'
986 initialize: function () {
989 clickSettings: function (event
) {
990 _kiwi
.app
.controlbox
.processInput('/settings');
996 _kiwi
.view
.Application
= Backbone
.View
.extend({
997 initialize: function () {
998 $(window
).resize(this.doLayout
);
999 $('#toolbar').resize(this.doLayout
);
1000 $('#controlbox').resize(this.doLayout
);
1002 // Change the theme when the config is changed
1003 _kiwi
.global
.settings
.on('change:theme', this.updateTheme
, this);
1004 this.updateTheme(getQueryVariable('theme'));
1008 $(document
).keydown(this.setKeyFocus
);
1010 // Confirmation require to leave the page
1011 window
.onbeforeunload = function () {
1012 if (_kiwi
.gateway
.isConnected()) {
1013 return 'This will close all KiwiIRC conversations. Are you sure you want to close this window?';
1020 updateTheme: function (theme_name
) {
1021 // If called by the settings callback, get the correct new_value
1022 if (theme_name
=== _kiwi
.global
.settings
) {
1023 theme_name
= arguments
[1];
1026 // If we have no theme specified, get it from the settings
1027 if (!theme_name
) theme_name
= _kiwi
.global
.settings
.get('theme');
1029 // Clear any current theme
1030 this.$el
.removeClass(function (i
, css
) {
1031 return (css
.match(/\btheme_\S+/g) || []).join(' ');
1034 // Apply the new theme
1035 this.$el
.addClass('theme_' + (theme_name
|| 'relaxed'));
1039 // Globally shift focus to the command input box on a keypress
1040 setKeyFocus: function (ev
) {
1041 // If we're copying text, don't shift focus
1042 if (ev
.ctrlKey
|| ev
.altKey
|| ev
.metaKey
) {
1046 // If we're typing into an input box somewhere, ignore
1047 if ((ev
.target
.tagName
.toLowerCase() === 'input') || $(ev
.target
).attr('contenteditable')) {
1051 $('#controlbox .inp').focus();
1055 doLayout: function () {
1056 var el_panels
= $('#panels');
1057 var el_memberlists
= $('#memberlists');
1058 var el_toolbar
= $('#toolbar');
1059 var el_controlbox
= $('#controlbox');
1060 var el_resize_handle
= $('#memberlists_resize_handle');
1063 top
: el_toolbar
.outerHeight(true),
1064 bottom
: el_controlbox
.outerHeight(true)
1068 // If any elements are not visible, full size the panals instead
1069 if (!el_toolbar
.is(':visible')) {
1070 css_heights
.top
= 0;
1073 if (!el_controlbox
.is(':visible')) {
1074 css_heights
.bottom
= 0;
1077 // Apply the CSS sizes
1078 el_panels
.css(css_heights
);
1079 el_memberlists
.css(css_heights
);
1080 el_resize_handle
.css(css_heights
);
1082 // Set the panels width depending on the memberlist visibility
1083 if (el_memberlists
.css('display') != 'none') {
1084 // Panels to the side of the memberlist
1085 el_panels
.css('right', el_memberlists
.outerWidth(true));
1086 // The resize handle sits overlapping the panels and memberlist
1087 el_resize_handle
.css('left', el_memberlists
.position().left
- (el_resize_handle
.outerWidth(true) / 2));
1089 // Memberlist is hidden so panels to the right edge
1090 el_panels
.css('right', 0);
1091 // And move the handle just out of sight to the right
1092 el_resize_handle
.css('left', el_panels
.outerWidth(true));
1097 alertWindow: function (title
) {
1098 if (!this.alertWindowTimer
) {
1099 this.alertWindowTimer
= new (function () {
1102 var has_focus
= true;
1104 var default_title
= 'Kiwi IRC';
1105 var title
= 'Kiwi IRC';
1107 this.setTitle = function (new_title
) {
1108 new_title
= new_title
|| default_title
;
1109 window
.document
.title
= new_title
;
1113 this.start = function (new_title
) {
1114 // Don't alert if we already have focus
1115 if (has_focus
) return;
1119 tmr
= setInterval(this.update
, 1000);
1122 this.stop = function () {
1123 // Stop the timer and clear the title
1124 if (tmr
) clearInterval(tmr
);
1128 // Some browsers don't always update the last title correctly
1129 // Wait a few seconds and then reset
1130 setTimeout(this.reset
, 2000);
1133 this.reset = function () {
1139 this.update = function () {
1141 that
.setTitle(title
);
1149 $(window
).focus(function (event
) {
1153 // Some browsers don't always update the last title correctly
1154 // Wait a few seconds and then reset
1155 setTimeout(that
.reset
, 2000);
1158 $(window
).blur(function (event
) {
1164 this.alertWindowTimer
.start(title
);
1168 barsHide: function (instant
) {
1172 $('#toolbar').slideUp({queue
: false, duration
: 400, step
: this.doLayout
});
1173 $('#controlbox').slideUp({queue
: false, duration
: 400, step
: this.doLayout
});
1175 $('#toolbar').slideUp(0);
1176 $('#controlbox').slideUp(0);
1181 barsShow: function (instant
) {
1185 $('#toolbar').slideDown({queue
: false, duration
: 400, step
: this.doLayout
});
1186 $('#controlbox').slideDown({queue
: false, duration
: 400, step
: this.doLayout
});
1188 $('#toolbar').slideDown(0);
1189 $('#controlbox').slideDown(0);
1203 _kiwi
.view
.MediaMessage
= Backbone
.View
.extend({
1205 'click .media_close': 'close'
1208 initialize: function () {
1209 // Get the URL from the data
1210 this.url
= this.$el
.data('url');
1213 // Close the media content and remove it from display
1214 close: function () {
1216 this.$content
.slideUp('fast', function () {
1217 that
.$content
.remove();
1221 // Open the media content within its wrapper
1223 // Create the content div if we haven't already
1224 if (!this.$content
) {
1225 this.$content
= $('<div class="media_content"><a class="media_close"><i class="icon-chevron-up"></i> Close media</a><br /><div class="content"></div></div>');
1226 this.$content
.find('.content').append(this.mediaTypes
[this.$el
.data('type')].apply(this, []) || 'Not found :(');
1229 // Now show the content if not already
1230 if (!this.$content
.is(':visible')) {
1231 // Hide it first so the slideDown always plays
1232 this.$content
.hide();
1234 // Add the media content and slide it into view
1235 this.$el
.append(this.$content
);
1236 this.$content
.slideDown();
1242 // Generate the media content for each recognised type
1244 twitter: function () {
1245 var tweet_id
= this.$el
.data('tweetid');
1248 $.getJSON('https://api.twitter.com/1/statuses/oembed.json?id=' + tweet_id
+ '&callback=?', function (data
) {
1249 that
.$content
.find('.content').html(data
.html
);
1252 return $('<div>Loading tweet..</div>');
1256 image: function () {
1257 return $('<a href="' + this.url
+ '" target="_blank"><img height="100" src="' + this.url
+ '" /></a>');
1261 reddit: function () {
1263 var matches
= (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(this.url
);
1265 $.getJSON('http://www.' + matches
[0] + '.json?jsonp=?', function (data
) {
1266 console
.log('Loaded reddit data', data
);
1267 var post
= data
[0].data
.children
[0].data
;
1270 // Show a thumbnail if there is one
1271 if (post
.thumbnail
) {
1272 //post.thumbnail = 'http://www.eurotunnel.com/uploadedImages/commercial/back-steps-icon-arrow.png';
1274 // Hide the thumbnail if an over_18 image
1276 thumb
= '<span class="thumbnail_nsfw" onclick="$(this).find(\'p\').remove(); $(this).find(\'img\').css(\'visibility\', \'visible\');">';
1277 thumb
+= '<p style="font-size:0.9em;line-height:1.2em;cursor:pointer;">Show<br />NSFW</p>';
1278 thumb
+= '<img src="' + post
.thumbnail
+ '" class="thumbnail" style="visibility:hidden;" />';
1281 thumb
= '<img src="' + post
.thumbnail
+ '" class="thumbnail" />';
1285 // Build the template string up
1286 var tmpl
= '<div>' + thumb
+ '<b><%- title %></b><br />Posted by <%- author %>. ';
1287 tmpl
+= '<i class="icon-arrow-up"></i> <%- ups %> <i class="icon-arrow-down"></i> <%- downs %><br />';
1288 tmpl
+= '<%- num_comments %> comments made. <a href="http://www.reddit.com<%- permalink %>">View post</a></div>';
1290 that
.$content
.find('.content').html(_
.template(tmpl
, post
));
1293 return $('<div>Loading Reddit thread..</div>');
1299 // Build the closed media HTML from a URL
1300 buildHtml: function (url
) {
1301 var html
= '', matches
;
1304 if (url
.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
1305 html
+= '<span class="media image" data-type="image" data-url="' + url
+ '" title="Open Image"><a class="open"><i class="icon-chevron-right"></i></a></span>';
1309 matches
= (/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/ig).exec(url
);
1311 html
+= '<span class="media twitter" data-type="twitter" data-url="' + url
+ '" data-tweetid="' + matches
[2] + '" title="Show tweet information"><a class="open"><i class="icon-chevron-right"></i></a></span>';
1315 matches
= (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(url
);
1317 html
+= '<span class="media reddit" data-type="reddit" data-url="' + url
+ '" title="Reddit thread"><a class="open"><i class="icon-chevron-right"></i></a></span>';