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 var prefix_css_class
= (member
.get('modes') || []).join(' ');
18 $('<li class="mode ' + prefix_css_class
+ '"><a class="nick"><span class="prefix">' + member
.get("prefix") + '</span>' + member
.get("nick") + '</a></li>')
20 .data('member', member
);
23 nickClick: function (event
) {
24 var $target
= $(event
.currentTarget
).parent('li'),
25 member
= $target
.data('member'),
28 event
.stopPropagation();
30 // If the userbox already exists here, hide it
31 if ($target
.find('.userbox').length
> 0) {
32 $('.userbox', this.$el
).remove();
36 userbox
= new _kiwi
.view
.UserBox();
37 userbox
.member
= member
;
38 userbox
.channel
= this.model
.channel
;
40 if (!this.model
.getByNick(_kiwi
.app
.connections
.active_connection
.get('nick')).get('is_op')) {
41 userbox
.$el
.children('.if_op').remove();
44 var menu
= new _kiwi
.view
.MenuBox(member
.get('nick') || 'User');
45 menu
.addItem('userbox', userbox
.$el
);
48 // Position the userbox + menubox
51 m_bottom
= t
+ menu
.$el
.outerHeight(), // Where the bottom of menu will be
52 memberlist_bottom
= this.$el
.parent().offset().top
+ this.$el
.parent().outerHeight();
54 // If the bottom of the userbox is going to be too low.. raise it
55 if (m_bottom
> memberlist_bottom
){
56 t
= memberlist_bottom
- menu
.$el
.outerHeight();
59 // Set the new positon
61 left
: _kiwi
.app
.view
.$el
.width() - menu
.$el
.outerWidth() - 20,
67 $('#memberlists').children().removeClass('active');
68 $(this.el
).addClass('active');
74 _kiwi
.view
.UserBox
= Backbone
.View
.extend({
76 'click .query': 'queryClick',
77 'click .info': 'infoClick',
78 'click .slap': 'slapClick',
79 'click .op': 'opClick',
80 'click .deop': 'deopClick',
81 'click .voice': 'voiceClick',
82 'click .devoice': 'devoiceClick',
83 'click .kick': 'kickClick',
84 'click .ban': 'banClick'
87 initialize: function () {
88 this.$el
= $($('#tmpl_userbox').html());
91 queryClick: function (event
) {
92 var panel
= new _kiwi
.model
.Query({name
: this.member
.get('nick')});
93 _kiwi
.app
.connections
.active_connection
.panels
.add(panel
);
97 infoClick: function (event
) {
98 _kiwi
.app
.controlbox
.processInput('/whois ' + this.member
.get('nick'));
101 slapClick: function (event
) {
102 _kiwi
.app
.controlbox
.processInput('/slap ' + this.member
.get('nick'));
105 opClick: function (event
) {
106 _kiwi
.app
.controlbox
.processInput('/mode ' + this.channel
.get('name') + ' +o ' + this.member
.get('nick'));
109 deopClick: function (event
) {
110 _kiwi
.app
.controlbox
.processInput('/mode ' + this.channel
.get('name') + ' -o ' + this.member
.get('nick'));
113 voiceClick: function (event
) {
114 _kiwi
.app
.controlbox
.processInput('/mode ' + this.channel
.get('name') + ' +v ' + this.member
.get('nick'));
117 devoiceClick: function (event
) {
118 _kiwi
.app
.controlbox
.processInput('/mode ' + this.channel
.get('name') + ' -v ' + this.member
.get('nick'));
121 kickClick: function (event
) {
122 // TODO: Enable the use of a custom kick message
123 _kiwi
.app
.controlbox
.processInput('/kick ' + this.member
.get('nick') + ' Bye!');
126 banClick: function (event
) {
127 // TODO: Set ban on host, not just on nick
128 _kiwi
.app
.controlbox
.processInput('/mode ' + this.channel
.get('name') + ' +b ' + this.member
.get('nick') + '!*');
132 _kiwi
.view
.NickChangeBox
= Backbone
.View
.extend({
134 'submit': 'changeNick',
135 'click .cancel': 'close'
138 initialize: function () {
139 this.$el
= $($('#tmpl_nickchange').html());
142 render: function () {
143 // Add the UI component and give it focus
144 _kiwi
.app
.controlbox
.$el
.prepend(this.$el
);
145 this.$el
.find('input').focus();
147 this.$el
.css('bottom', _kiwi
.app
.controlbox
.$el
.outerHeight(true));
155 changeNick: function (event
) {
158 event
.preventDefault();
160 _kiwi
.app
.connections
.active_connection
.gateway
.changeNick(this.$el
.find('input').val(), function (err
, val
) {
167 _kiwi
.view
.ServerSelect = function () {
168 // Are currently showing all the controlls or just a nick_change box?
171 var model
= Backbone
.View
.extend({
173 'submit form': 'submitForm',
174 'click .show_more': 'showMore',
175 'change .have_pass input': 'showPass'
178 initialize: function () {
181 this.$el
= $($('#tmpl_server_select').html());
183 // Remove the 'more' link if the server has disabled server changing
184 if (_kiwi
.app
.server_settings
&& _kiwi
.app
.server_settings
.connection
) {
185 if (!_kiwi
.app
.server_settings
.connection
.allow_change
) {
186 this.$el
.find('.show_more').remove();
187 this.$el
.addClass('single_server');
191 _kiwi
.gateway
.bind('onconnect', this.networkConnected
, this);
192 _kiwi
.gateway
.bind('connecting', this.networkConnecting
, this);
193 _kiwi
.gateway
.bind('onirc_error', this.onIrcError
, this);
196 dispose: function() {
197 _kiwi
.gateway
.off('onconnect', this.networkConnected
, this);
198 _kiwi
.gateway
.off('connecting', this.networkConnecting
, this);
199 _kiwi
.gateway
.off('onirc_error', this.onIrcError
, this);
204 submitForm: function (event
) {
205 event
.preventDefault();
207 // Make sure a nick is chosen
208 if (!$('input.nick', this.$el
).val().trim()) {
209 this.setStatus('Select a nickname first!');
210 $('input.nick', this.$el
).select();
214 if (state
=== 'nick_change') {
215 this.submitNickChange(event
);
217 this.submitLogin(event
);
220 $('button', this.$el
).attr('disabled', 1);
224 submitLogin: function (event
) {
225 // If submitting is disabled, don't do anything
226 if ($('button', this.$el
).attr('disabled')) return;
229 nick
: $('input.nick', this.$el
).val(),
230 server
: $('input.server', this.$el
).val(),
231 port
: $('input.port', this.$el
).val(),
232 ssl
: $('input.ssl', this.$el
).prop('checked'),
233 password
: $('input.password', this.$el
).val(),
234 channel
: $('input.channel', this.$el
).val(),
235 channel_key
: $('input.channel_key', this.$el
).val()
238 this.trigger('server_connect', values
);
241 submitNickChange: function (event
) {
242 _kiwi
.gateway
.changeNick(null, $('input.nick', this.$el
).val());
243 this.networkConnecting();
246 showPass: function (event
) {
247 if (this.$el
.find('tr.have_pass input').is(':checked')) {
248 this.$el
.find('tr.pass').show().find('input').focus();
250 this.$el
.find('tr.pass').hide().find('input').val('');
254 showMore: function (event
) {
255 $('.more', this.$el
).slideDown('fast');
256 $('input.server', this.$el
).select();
259 populateFields: function (defaults
) {
260 var nick
, server
, port
, channel
, channel_key
, ssl
, password
;
262 defaults
= defaults
|| {};
264 nick
= defaults
.nick
|| '';
265 server
= defaults
.server
|| '';
266 port
= defaults
.port
|| 6667;
267 ssl
= defaults
.ssl
|| 0;
268 password
= defaults
.password
|| '';
269 channel
= defaults
.channel
|| '';
270 channel_key
= defaults
.channel_key
|| '';
272 $('input.nick', this.$el
).val(nick
);
273 $('input.server', this.$el
).val(server
);
274 $('input.port', this.$el
).val(port
);
275 $('input.ssl', this.$el
).prop('checked', ssl
);
276 $('input.password', this.$el
).val(password
);
277 $('input.channel', this.$el
).val(channel
);
278 $('input.channel_key', this.$el
).val(channel_key
);
285 show: function (new_state
) {
286 new_state
= new_state
|| 'all';
290 if (new_state
=== 'all') {
291 $('.show_more', this.$el
).show();
293 } else if (new_state
=== 'more') {
294 $('.more', this.$el
).slideDown('fast');
296 } else if (new_state
=== 'nick_change') {
297 $('.more', this.$el
).hide();
298 $('.show_more', this.$el
).hide();
299 $('input.nick', this.$el
).select();
305 setStatus: function (text
, class_name
) {
306 $('.status', this.$el
)
308 .attr('class', 'status')
309 .addClass(class_name
||'')
312 clearStatus: function () {
313 $('.status', this.$el
).hide();
316 networkConnected: function (event
) {
317 this.setStatus('Connected :)', 'ok');
318 $('form', this.$el
).hide();
321 networkConnecting: function (event
) {
322 this.setStatus('Connecting..', 'ok');
325 onIrcError: function (data
) {
326 $('button', this.$el
).attr('disabled', null);
328 if (data
.error
== 'nickname_in_use') {
329 this.setStatus('Nickname already taken');
330 this.show('nick_change');
333 if (data
.error
== 'password_mismatch') {
334 this.setStatus('Incorrect Password');
335 this.show('nick_change');
336 that
.$el
.find('.password').select();
340 showError: function (event
) {
341 this.setStatus('Error connecting', 'error');
342 $('button', this.$el
).attr('disabled', null);
348 return new model(arguments
);
352 _kiwi
.view
.Panel
= Backbone
.View
.extend({
354 className
: "panel messages",
357 "click .chan": "chanClick",
358 'click .media .open': 'mediaClick',
359 'mouseenter .msg .nick': 'msgEnter',
360 'mouseleave .msg .nick': 'msgLeave'
363 initialize: function (options
) {
364 this.initializePanel(options
);
367 initializePanel: function (options
) {
368 this.$el
.css('display', 'none');
369 options
= options
|| {};
371 // Containing element for this panel
372 if (options
.container
) {
373 this.$container
= $(options
.container
);
375 this.$container
= $('#panels .container1');
378 this.$el
.appendTo(this.$container
);
380 this.alert_level
= 0;
382 this.model
.bind('msg', this.newMsg
, this);
385 this.model
.set({"view": this}, {"silent": true});
388 render: function () {
392 _
.each(this.model
.get('scrollback'), function (msg
) {
397 newMsg: function (msg
) {
398 var re
, line_msg
, $this = this.$el
,
399 nick_colour_hex
, nick_hex
, is_highlight
, msg_css_classes
= '';
401 // Nick highlight detecting
402 if ((new RegExp('\\b' + _kiwi
.app
.connections
.active_connection
.get('nick') + '\\b', 'i')).test(msg
.msg
)) {
404 msg_css_classes
+= ' highlight';
407 // Escape any HTML that may be in here
408 msg
.msg
= $('<div />').text(msg
.msg
).html();
410 // Make the channels clickable
411 re
= new RegExp('(?:^|\\s)([' + _kiwi
.gateway
.get('channel_prefix') + '][^ ,.\\007]+)', 'g');
412 msg
.msg
= msg
.msg
.replace(re
, function (match
) {
413 return '<a class="chan" data-channel="' + match
.trim() + '">' + match
+ '</a>';
417 // Parse any links found
418 msg
.msg
= msg
.msg
.replace(/(([A-Za-z0-9\-]+\:\/\/)|(www\.))([\w.\-]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w#!:.?$'()[\]*,;~+=&%@!\-\/]*)?/gi, function (url
) {
422 // Add the http if no protoocol was found
423 if (url
.match(/^www\./)) {
424 url
= 'http://' + url
;
427 // Shorten the displayed URL if it's going to be too long
428 if (nice
.length
> 100) {
429 nice
= nice
.substr(0, 100) + '...';
432 // Get any media HTML if supported
433 extra_html
= _kiwi
.view
.MediaMessage
.buildHtml(url
);
435 // Make the link clickable
436 return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url
+ '">' + nice
+ '</a> ' + extra_html
;
440 // Convert IRC formatting into HTML formatting
441 msg
.msg
= formatIRCMsg(msg
.msg
);
444 // Add some colours to the nick (Method based on IRSSIs nickcolor.pl)
445 nick_colour_hex
= (function (nick
) {
446 var nick_int
= 0, rgb
;
448 _
.map(nick
.split(''), function (i
) { nick_int
+= i
.charCodeAt(0); });
449 rgb
= hsl2rgb(nick_int
% 255, 70, 35);
450 rgb
= rgb
[2] | (rgb
[1] << 8) | (rgb
[0] << 16);
452 return '#' + rgb
.toString(16);
455 msg
.nick_style
= 'color:' + nick_colour_hex
+ ';';
457 // Generate a hex string from the nick to be used as a CSS class name
458 nick_hex
= msg
.nick_css_class
= '';
460 _
.map(msg
.nick
.split(''), function (char) {
461 nick_hex
+= char.charCodeAt(0).toString(16);
463 msg_css_classes
+= ' nick_' + nick_hex
;
466 // Build up and add the line
467 msg
.msg_css_classes
= msg_css_classes
;
468 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>';
469 $this.append(_
.template(line_msg
, msg
));
471 // Activity/alerts based on the type of new message
472 if (msg
.type
.match(/^action /)) {
473 this.alert('action');
475 } else if (is_highlight
) {
476 _kiwi
.app
.view
.alertWindow('* People are talking!');
477 _kiwi
.app
.view
.playSound('highlight');
478 this.alert('highlight');
481 // If this is the active panel, send an alert out
482 if (this.model
.isActive()) {
483 _kiwi
.app
.view
.alertWindow('* People are talking!');
485 this.alert('activity');
488 if (this.model
.isQuery() && !this.model
.isActive()) {
489 _kiwi
.app
.view
.alertWindow('* People are talking!');
490 _kiwi
.app
.view
.playSound('highlight');
493 // Update the activity counters
495 // Only inrement the counters if we're not the active panel
496 if (this.model
.isActive()) return;
498 var $act
= this.model
.tab
.find('.activity');
499 $act
.text((parseInt($act
.text(), 10) || 0) + 1);
500 if ($act
.text() === '0') {
501 $act
.addClass('zero');
503 $act
.removeClass('zero');
507 this.scrollToBottom();
509 // Make sure our DOM isn't getting too large (Acts as scrollback)
511 if (this.msg_count
> (parseInt(_kiwi
.global
.settings
.get('scrollback'), 10) || 250)) {
512 $('.msg:first', this.$el
).remove();
516 chanClick: function (event
) {
518 _kiwi
.gateway
.join(null, $(event
.target
).data('channel'));
521 _kiwi
.gateway
.join(null, $(event
.srcElement
).data('channel'));
525 mediaClick: function (event
) {
526 var $media
= $(event
.target
).parents('.media');
529 if ($media
.data('media')) {
530 media_message
= $media
.data('media');
532 media_message
= new _kiwi
.view
.MediaMessage({el
: $media
[0]});
533 $media
.data('media', media_message
);
536 $media
.data('media', media_message
);
538 media_message
.open();
541 // Cursor hovers over a message
542 msgEnter: function (event
) {
545 // Find a valid class that this element has
546 _
.each($(event
.currentTarget
).parent('.msg').attr('class').split(' '), function (css_class
) {
547 if (css_class
.match(/^nick_[a-z0-9]+/i)) {
548 nick_class
= css_class
;
552 // If no class was found..
553 if (!nick_class
) return;
555 $('.'+nick_class
).addClass('global_nick_highlight');
558 // Cursor leaves message
559 msgLeave: function (event
) {
562 // Find a valid class that this element has
563 _
.each($(event
.currentTarget
).parent('.msg').attr('class').split(' '), function (css_class
) {
564 if (css_class
.match(/^nick_[a-z0-9]+/i)) {
565 nick_class
= css_class
;
569 // If no class was found..
570 if (!nick_class
) return;
572 $('.'+nick_class
).removeClass('global_nick_highlight');
576 var $this = this.$el
;
578 // Hide all other panels and show this one
579 this.$container
.children('.panel').css('display', 'none');
580 $this.css('display', 'block');
582 // Show this panels memberlist
583 var members
= this.model
.get("members");
585 $('#memberlists').removeClass('disabled');
588 // Memberlist not found for this panel, hide any active ones
589 $('#memberlists').addClass('disabled').children().removeClass('active');
592 // Remove any alerts and activity counters for this panel
594 this.model
.tab
.find('.activity').text('0').addClass('zero');
596 _kiwi
.app
.panels
.trigger('active', this.model
, _kiwi
.app
.panels().active
);
597 this.model
.trigger('active', this.model
);
599 _kiwi
.app
.view
.doLayout();
601 this.scrollToBottom(true);
605 alert: function (level
) {
606 // No need to highlight if this si the active panel
607 if (this.model
== _kiwi
.app
.panels().active
) return;
610 types
= ['none', 'action', 'activity', 'highlight'];
612 // Default alert level
613 level
= level
|| 'none';
615 // If this alert level does not exist, assume clearing current level
616 type_idx
= _
.indexOf(types
, level
);
622 // Only 'upgrade' the alert. Never down (unless clearing)
623 if (type_idx
!== 0 && type_idx
<= this.alert_level
) {
627 // Clear any existing levels
628 this.model
.tab
.removeClass(function (i
, css
) {
629 return (css
.match(/\balert_\S+/g) || []).join(' ');
632 // Add the new level if there is one
633 if (level
!== 'none') {
634 this.model
.tab
.addClass('alert_' + level
);
637 this.alert_level
= type_idx
;
641 // Scroll to the bottom of the panel
642 scrollToBottom: function (force_down
) {
643 // If this isn't the active panel, don't scroll
644 if (this.model
!== _kiwi
.app
.panels().active
) return;
646 // Don't scroll down if we're scrolled up the panel a little
647 if (force_down
|| this.$container
.scrollTop() + this.$container
.height() > this.$el
.outerHeight() - 150) {
648 this.$container
[0].scrollTop
= this.$container
[0].scrollHeight
;
653 _kiwi
.view
.Applet
= _kiwi
.view
.Panel
.extend({
655 initialize: function (options
) {
656 this.initializePanel(options
);
660 _kiwi
.view
.Channel
= _kiwi
.view
.Panel
.extend({
661 initialize: function (options
) {
662 this.initializePanel(options
);
663 this.model
.bind('change:topic', this.topic
, this);
665 // Only show the loader if this is a channel (ie. not a query)
666 if (this.model
.isChannel()) {
667 this.$el
.append('<div class="initial_loader" style="margin:1em;text-align:center;">Joining channel.. <span class="loader"></span></div>');
671 // Override the existing newMsg() method to remove the joining channel loader
672 newMsg: function () {
673 this.$el
.find('.initial_loader').slideUp(function () {
677 return this.constructor.__super__
.newMsg
.apply(this, arguments
);
680 topic: function (topic
) {
681 if (typeof topic
!== 'string' || !topic
) {
682 topic
= this.model
.get("topic");
685 this.model
.addMsg('', '== Topic for ' + this.model
.get('name') + ' is: ' + topic
, 'topic');
687 // If this is the active channel then update the topic bar
688 if (_kiwi
.app
.panels().active
=== this) {
689 _kiwi
.app
.topicbar
.setCurrentTopic(this.model
.get("topic"));
696 // Model for this = _kiwi.model.NetworkPanelList
697 _kiwi
.view
.NetworkTabs
= Backbone
.View
.extend({
699 className
: 'connections',
701 initialize: function() {
702 this.model
.on('add', this.networkAdded
, this);
703 this.model
.on('remove', this.networkRemoved
, this);
705 this.$el
.appendTo($('#kiwi #tabs'));
708 networkAdded: function(network
) {
709 $('<li class="connection"></li>')
710 .append(network
.panels
.view
.$el
)
714 networkRemoved: function(network
) {
715 network
.panels
.view
.remove();
717 _kiwi
.app
.view
.doLayout();
723 // Model for this = _kiwi.model.PanelList
724 _kiwi
.view
.Tabs
= Backbone
.View
.extend({
726 className
: 'panellist',
729 'click li': 'tabClick',
730 'click li .part': 'partClick'
733 initialize: function () {
734 this.model
.on("add", this.panelAdded
, this);
735 this.model
.on("remove", this.panelRemoved
, this);
736 this.model
.on("reset", this.render
, this);
738 this.model
.on('active', this.panelActive
, this);
740 // Network tabs start with a server, so determine what we are now
741 this.is_network
= false;
743 if (this.model
.network
) {
744 this.is_network
= true;
746 this.model
.network
.on('change:name', function (network
, new_val
) {
747 $('span', this.model
.server
.tab
).text(new_val
);
752 render: function () {
757 if (this.is_network
) {
758 // Add the server tab first
759 this.model
.server
.tab
760 .data('panel', this.model
.server
)
761 .data('connection_id', this.model
.network
.get('connection_id'))
765 // Go through each panel adding its tab
766 this.model
.forEach(function (panel
) {
767 // If this is the server panel, ignore as it's already added
768 if (this.is_network
&& panel
== that
.model
.server
)
771 panel
.tab
.data('panel', panel
);
774 panel
.tab
.data('connection_id', this.model
.network
.get('connection_id'));
776 panel
.tab
.appendTo(that
.$el
);
779 _kiwi
.app
.view
.doLayout();
782 updateTabTitle: function (panel
, new_title
) {
783 $('span', panel
.tab
).text(new_title
);
786 panelAdded: function (panel
) {
787 // Add a tab to the panel
788 panel
.tab
= $('<li><span>' + (panel
.get('title') || panel
.get('name')) + '</span><div class="activity"></div></li>');
790 if (panel
.isServer()) {
791 panel
.tab
.addClass('server');
792 panel
.tab
.addClass('icon-nonexistant');
795 panel
.tab
.data('panel', panel
);
798 panel
.tab
.data('connection_id', this.model
.network
.get('connection_id'));
800 panel
.tab
.appendTo(this.$el
);
802 panel
.bind('change:title', this.updateTabTitle
);
803 panel
.bind('change:name', this.updateTabTitle
);
805 _kiwi
.app
.view
.doLayout();
807 panelRemoved: function (panel
) {
811 _kiwi
.app
.view
.doLayout();
814 panelActive: function (panel
, previously_active_panel
) {
815 // Remove any existing tabs or part images
816 _kiwi
.app
.view
.$el
.find('.panellist .part').remove();
817 _kiwi
.app
.view
.$el
.find('.panellist .active').removeClass('active');
819 panel
.tab
.addClass('active');
821 // Only show the part image on non-server tabs
822 if (!panel
.isServer()) {
823 panel
.tab
.append('<span class="part icon-nonexistant"></span>');
827 tabClick: function (e
) {
828 var tab
= $(e
.currentTarget
);
830 var panel
= tab
.data('panel');
832 // A panel wasn't found for this tab... wadda fuck
839 partClick: function (e
) {
840 var tab
= $(e
.currentTarget
).parent();
841 var panel
= tab
.data('panel');
845 // Only need to part if it's a channel
846 // If the nicklist is empty, we haven't joined the channel as yet
847 if (panel
.isChannel() && panel
.get('members').models
.length
> 0) {
848 this.model
.network
.gateway
.part(panel
.get('name'));
857 _kiwi
.view
.TopicBar
= Backbone
.View
.extend({
859 'keydown div': 'process'
862 initialize: function () {
863 _kiwi
.app
.panels
.bind('active', function (active_panel
) {
864 // If it's a channel topic, update and make editable
865 if (active_panel
.isChannel()) {
866 this.setCurrentTopic(active_panel
.get('topic') || '');
867 this.$el
.find('div').attr('contentEditable', true);
870 // Not a channel topic.. clear and make uneditable
871 this.$el
.find('div').attr('contentEditable', false)
877 process: function (ev
) {
878 var inp
= $(ev
.currentTarget
),
879 inp_val
= inp
.text();
881 // Only allow topic editing if this is a channel panel
882 if (!_kiwi
.app
.panels().active
.isChannel()) {
886 // If hit return key, update the current topic
887 if (ev
.keyCode
=== 13) {
888 _kiwi
.gateway
.topic(null, _kiwi
.app
.panels().active
.get('name'), inp_val
);
893 setCurrentTopic: function (new_topic
) {
894 new_topic
= new_topic
|| '';
896 // We only want a plain text version
897 $('div', this.$el
).html(formatIRCMsg(_
.escape(new_topic
)));
903 _kiwi
.view
.ControlBox
= Backbone
.View
.extend({
905 'keydown .inp': 'process',
906 'click .nick': 'showNickChange'
909 initialize: function () {
912 this.buffer
= []; // Stores previously run commands
913 this.buffer_pos
= 0; // The current position in the buffer
915 this.preprocessor
= new InputPreProcessor();
916 this.preprocessor
.recursive_depth
= 5;
918 // Hold tab autocomplete data
919 this.tabcomplete
= {active
: false, data
: [], prefix
: ''};
921 // Keep the nick view updated with nick changes
922 _kiwi
.app
.connections
.on('change:nick', function(connection
) {
923 // Only update the nick view if it's the active connection
924 if (connection
!== _kiwi
.app
.connections
.active_connection
)
927 $('.nick', that
.$el
).text(connection
.get('nick'));
930 // Update our nick view as we flick between connections
931 _kiwi
.app
.connections
.on('active', function(panel
, connection
) {
932 $('.nick', that
.$el
).text(connection
.get('nick'));
936 showNickChange: function (ev
) {
937 (new _kiwi
.view
.NickChangeBox()).render();
940 process: function (ev
) {
942 inp
= $(ev
.currentTarget
),
946 if (navigator
.appVersion
.indexOf("Mac") !== -1) {
952 // If not a tab key, reset the tabcomplete data
953 if (this.tabcomplete
.active
&& ev
.keyCode
!== 9) {
954 this.tabcomplete
.active
= false;
955 this.tabcomplete
.data
= [];
956 this.tabcomplete
.prefix
= '';
960 case (ev
.keyCode
=== 13): // return
961 inp_val
= inp_val
.trim();
964 $.each(inp_val
.split('\n'), function (idx
, line
) {
965 that
.processInput(line
);
968 this.buffer
.push(inp_val
);
969 this.buffer_pos
= this.buffer
.length
;
977 case (ev
.keyCode
=== 38): // up
978 if (this.buffer_pos
> 0) {
980 inp
.val(this.buffer
[this.buffer_pos
]);
984 case (ev
.keyCode
=== 40): // down
985 if (this.buffer_pos
< this.buffer
.length
) {
987 inp
.val(this.buffer
[this.buffer_pos
]);
991 case (ev
.keyCode
=== 219 && meta
): // [ + meta
992 // Find all the tab elements and get the index of the active tab
993 var $tabs
= $('#kiwi #tabs').find('li[class!=connection]');
994 var cur_tab_ind
= (function() {
995 for (var idx
=0; idx
<$tabs
.length
; idx
++){
996 if ($($tabs
[idx
]).hasClass('active'))
1001 // Work out the previous tab along. Wrap around if needed
1002 if (cur_tab_ind
=== 0) {
1003 $prev_tab
= $($tabs
[$tabs
.length
- 1]);
1005 $prev_tab
= $($tabs
[cur_tab_ind
- 1]);
1011 case (ev
.keyCode
=== 221 && meta
): // ] + meta
1012 // Find all the tab elements and get the index of the active tab
1013 var $tabs
= $('#kiwi #tabs').find('li[class!=connection]');
1014 var cur_tab_ind
= (function() {
1015 for (var idx
=0; idx
<$tabs
.length
; idx
++){
1016 if ($($tabs
[idx
]).hasClass('active'))
1021 // Work out the next tab along. Wrap around if needed
1022 if (cur_tab_ind
=== $tabs
.length
- 1) {
1023 $next_tab
= $($tabs
[0]);
1025 $next_tab
= $($tabs
[cur_tab_ind
+ 1]);
1031 case (ev
.keyCode
=== 9): // tab
1032 this.tabcomplete
.active
= true;
1033 if (_
.isEqual(this.tabcomplete
.data
, [])) {
1034 // Get possible autocompletions
1036 members
= _kiwi
.app
.panels().active
.get('members');
1038 // If we have a members list, get the models. Otherwise empty array
1039 members
= members
? members
.models
: [];
1041 $.each(members
, function (i
, member
) {
1042 if (!member
) return;
1043 ac_data
.push(member
.get('nick'));
1046 ac_data
.push(_kiwi
.app
.panels().active
.get('name'));
1048 ac_data
= _
.sortBy(ac_data
, function (nick
) {
1051 this.tabcomplete
.data
= ac_data
;
1054 if (inp_val
[inp
[0].selectionStart
- 1] === ' ') {
1059 var tokens
, // Words before the cursor position
1060 val
, // New value being built up
1061 p1
, // Position in the value just before the nick
1062 newnick
, // New nick to be displayed (cycles through)
1063 range
, // TextRange for setting new text cursor position
1064 nick
, // Current nick in the value
1065 trailing
= ': '; // Text to be inserted after a tabbed nick
1067 tokens
= inp_val
.substring(0, inp
[0].selectionStart
).split(' ');
1068 if (tokens
[tokens
.length
-1] == ':')
1071 nick
= tokens
[tokens
.length
- 1];
1073 if (this.tabcomplete
.prefix
=== '') {
1074 this.tabcomplete
.prefix
= nick
;
1077 this.tabcomplete
.data
= _
.select(this.tabcomplete
.data
, function (n
) {
1078 return (n
.toLowerCase().indexOf(that
.tabcomplete
.prefix
.toLowerCase()) === 0);
1081 if (this.tabcomplete
.data
.length
> 0) {
1082 // Get the current value before cursor position
1083 p1
= inp
[0].selectionStart
- (nick
.length
);
1084 val
= inp_val
.substr(0, p1
);
1086 // Include the current selected nick
1087 newnick
= this.tabcomplete
.data
.shift();
1088 this.tabcomplete
.data
.push(newnick
);
1091 if (inp_val
.substr(inp
[0].selectionStart
, 2) !== trailing
)
1094 // Now include the rest of the current value
1095 val
+= inp_val
.substr(inp
[0].selectionStart
);
1099 // Move the cursor position to the end of the nick
1100 if (inp
[0].setSelectionRange
) {
1101 inp
[0].setSelectionRange(p1
+ newnick
.length
+ trailing
.length
, p1
+ newnick
.length
+ trailing
.length
);
1102 } else if (inp
[0].createTextRange
) { // not sure if this bit is actually needed....
1103 range
= inp
[0].createTextRange();
1104 range
.collapse(true);
1105 range
.moveEnd('character', p1
+ newnick
.length
+ trailing
.length
);
1106 range
.moveStart('character', p1
+ newnick
.length
+ trailing
.length
);
1116 processInput: function (command_raw
) {
1117 var command
, params
,
1120 // The default command
1121 if (command_raw
[0] !== '/' || command_raw
.substr(0, 2) === '//') {
1122 // Remove any slash escaping at the start (ie. //)
1123 command_raw
= command_raw
.replace(/^\/\//, '/');
1125 // Prepend the default command
1126 command_raw
= '/msg ' + _kiwi
.app
.panels().active
.get('name') + ' ' + command_raw
;
1129 // Process the raw command for any aliases
1130 this.preprocessor
.vars
.server
= _kiwi
.app
.connections
.active_connection
.get('name');
1131 this.preprocessor
.vars
.channel
= _kiwi
.app
.panels().active
.get('name');
1132 this.preprocessor
.vars
.destination
= this.preprocessor
.vars
.channel
;
1133 command_raw
= this.preprocessor
.process(command_raw
);
1135 // Extract the command and parameters
1136 params
= command_raw
.split(' ');
1137 if (params
[0][0] === '/') {
1138 command
= params
[0].substr(1).toLowerCase();
1139 params
= params
.splice(1, params
.length
- 1);
1143 params
.unshift(_kiwi
.app
.panels().active
.get('name'));
1146 // Trigger the command events
1147 this.trigger('command', {command
: command
, params
: params
});
1148 this.trigger('command:' + command
, {command
: command
, params
: params
});
1150 // If we didn't have any listeners for this event, fire a special case
1151 // TODO: This feels dirty. Should this really be done..?
1152 if (!this._events
['command:' + command
]) {
1153 this.trigger('unknown_command', {command
: command
, params
: params
});
1158 addPluginIcon: function ($icon
) {
1159 var $tool
= $('<div class="tool"></div>').append($icon
);
1160 this.$el
.find('.input_tools').append($tool
);
1161 _kiwi
.app
.view
.doLayout();
1168 _kiwi
.view
.StatusMessage
= Backbone
.View
.extend({
1169 initialize: function () {
1172 // Timer for hiding the message after X seconds
1176 text: function (text
, opt
) {
1179 opt
.type
= opt
.type
|| '';
1180 opt
.timeout
= opt
.timeout
|| 5000;
1182 this.$el
.text(text
).attr('class', opt
.type
);
1183 this.$el
.slideDown($.proxy(_kiwi
.app
.view
.doLayout
, this));
1185 if (opt
.timeout
) this.doTimeout(opt
.timeout
);
1188 html: function (html
, opt
) {
1191 opt
.type
= opt
.type
|| '';
1192 opt
.timeout
= opt
.timeout
|| 5000;
1194 this.$el
.html(text
).attr('class', opt
.type
);
1195 this.$el
.slideDown(_kiwi
.app
.view
.doLayout
);
1197 if (opt
.timeout
) this.doTimeout(opt
.timeout
);
1201 this.$el
.slideUp($.proxy(_kiwi
.app
.view
.doLayout
, this));
1204 doTimeout: function (length
) {
1205 if (this.tmr
) clearTimeout(this.tmr
);
1207 this.tmr
= setTimeout(function () { that
.hide(); }, length
);
1214 _kiwi
.view
.ResizeHandler
= Backbone
.View
.extend({
1216 'mousedown': 'startDrag',
1217 'mouseup': 'stopDrag'
1220 initialize: function () {
1221 this.dragging
= false;
1222 this.starting_width
= {};
1224 $(window
).on('mousemove', $.proxy(this.onDrag
, this));
1227 startDrag: function (event
) {
1228 this.dragging
= true;
1231 stopDrag: function (event
) {
1232 this.dragging
= false;
1235 onDrag: function (event
) {
1236 if (!this.dragging
) return;
1238 this.$el
.css('left', event
.clientX
- (this.$el
.outerWidth(true) / 2));
1239 $('#memberlists').css('width', this.$el
.parent().width() - (this.$el
.position().left
+ this.$el
.outerWidth()));
1240 _kiwi
.app
.view
.doLayout();
1246 _kiwi
.view
.AppToolbar
= Backbone
.View
.extend({
1248 'click .settings': 'clickSettings'
1251 initialize: function () {
1254 clickSettings: function (event
) {
1255 _kiwi
.app
.controlbox
.processInput('/settings');
1261 _kiwi
.view
.Application
= Backbone
.View
.extend({
1262 initialize: function () {
1265 $(window
).resize(function() { that
.doLayout
.apply(that
); });
1266 $('#toolbar').resize(function() { that
.doLayout
.apply(that
); });
1267 $('#controlbox').resize(function() { that
.doLayout
.apply(that
); });
1269 // Change the theme when the config is changed
1270 _kiwi
.global
.settings
.on('change:theme', this.updateTheme
, this);
1271 this.updateTheme(getQueryVariable('theme'));
1273 _kiwi
.global
.settings
.on('change:channel_list_style', this.setTabLayout
, this);
1274 this.setTabLayout(_kiwi
.global
.settings
.get('channel_list_style'));
1276 _kiwi
.global
.settings
.on('change:show_timestamps', this.displayTimestamps
, this);
1277 this.displayTimestamps(_kiwi
.global
.settings
.get('show_timestamps'));
1281 $(document
).keydown(this.setKeyFocus
);
1283 // Confirmation require to leave the page
1284 window
.onbeforeunload = function () {
1285 if (_kiwi
.gateway
.isConnected()) {
1286 return 'This will close all KiwiIRC conversations. Are you sure you want to close this window?';
1295 updateTheme: function (theme_name
) {
1296 // If called by the settings callback, get the correct new_value
1297 if (theme_name
=== _kiwi
.global
.settings
) {
1298 theme_name
= arguments
[1];
1301 // If we have no theme specified, get it from the settings
1302 if (!theme_name
) theme_name
= _kiwi
.global
.settings
.get('theme');
1304 // Clear any current theme
1305 this.$el
.removeClass(function (i
, css
) {
1306 return (css
.match(/\btheme_\S+/g) || []).join(' ');
1309 // Apply the new theme
1310 this.$el
.addClass('theme_' + (theme_name
|| 'relaxed'));
1314 setTabLayout: function (layout_style
) {
1315 // If called by the settings callback, get the correct new_value
1316 if (layout_style
=== _kiwi
.global
.settings
) {
1317 layout_style
= arguments
[1];
1320 if (layout_style
== 'list') {
1321 this.$el
.addClass('chanlist_treeview');
1323 this.$el
.removeClass('chanlist_treeview');
1330 displayTimestamps: function (show_timestamps
) {
1331 // If called by the settings callback, get the correct new_value
1332 if (show_timestamps
=== _kiwi
.global
.settings
) {
1333 show_timestamps
= arguments
[1];
1336 if (show_timestamps
) {
1337 this.$el
.addClass('timestamps');
1339 this.$el
.removeClass('timestamps');
1344 // Globally shift focus to the command input box on a keypress
1345 setKeyFocus: function (ev
) {
1346 // If we're copying text, don't shift focus
1347 if (ev
.ctrlKey
|| ev
.altKey
|| ev
.metaKey
) {
1351 // If we're typing into an input box somewhere, ignore
1352 if ((ev
.target
.tagName
.toLowerCase() === 'input') || (ev
.target
.tagName
.toLowerCase() === 'textarea') || $(ev
.target
).attr('contenteditable')) {
1356 $('#controlbox .inp').focus();
1360 doLayout: function () {
1361 var el_kiwi
= this.$el
;
1362 var el_panels
= $('#kiwi #panels');
1363 var el_memberlists
= $('#kiwi #memberlists');
1364 var el_toolbar
= $('#kiwi #toolbar');
1365 var el_controlbox
= $('#kiwi #controlbox');
1366 var el_resize_handle
= $('#kiwi #memberlists_resize_handle');
1369 top
: el_toolbar
.outerHeight(true),
1370 bottom
: el_controlbox
.outerHeight(true)
1374 // If any elements are not visible, full size the panals instead
1375 if (!el_toolbar
.is(':visible')) {
1376 css_heights
.top
= 0;
1379 if (!el_controlbox
.is(':visible')) {
1380 css_heights
.bottom
= 0;
1383 // Apply the CSS sizes
1384 el_panels
.css(css_heights
);
1385 el_memberlists
.css(css_heights
);
1386 el_resize_handle
.css(css_heights
);
1388 // If we have channel tabs on the side, adjust the height
1389 if (el_kiwi
.hasClass('chanlist_treeview')) {
1390 $('#tabs', el_kiwi
).css(css_heights
);
1393 // Determine if we have a narrow window (mobile/tablet/or even small desktop window)
1394 if (el_kiwi
.outerWidth() < 400) {
1395 el_kiwi
.addClass('narrow');
1397 el_kiwi
.removeClass('narrow');
1400 // Set the panels width depending on the memberlist visibility
1401 if (el_memberlists
.css('display') != 'none') {
1402 // Panels to the side of the memberlist
1403 el_panels
.css('right', el_memberlists
.outerWidth(true));
1404 // The resize handle sits overlapping the panels and memberlist
1405 el_resize_handle
.css('left', el_memberlists
.position().left
- (el_resize_handle
.outerWidth(true) / 2));
1407 // Memberlist is hidden so panels to the right edge
1408 el_panels
.css('right', 0);
1409 // And move the handle just out of sight to the right
1410 el_resize_handle
.css('left', el_panels
.outerWidth(true));
1413 var input_wrap_width
= parseInt($('#kiwi #controlbox .input_tools').outerWidth());
1414 el_controlbox
.find('.input_wrap').css('right', input_wrap_width
+ 7);
1418 alertWindow: function (title
) {
1419 if (!this.alertWindowTimer
) {
1420 this.alertWindowTimer
= new (function () {
1423 var has_focus
= true;
1425 var default_title
= 'Kiwi IRC';
1426 var title
= 'Kiwi IRC';
1428 this.setTitle = function (new_title
) {
1429 new_title
= new_title
|| default_title
;
1430 window
.document
.title
= new_title
;
1434 this.start = function (new_title
) {
1435 // Don't alert if we already have focus
1436 if (has_focus
) return;
1440 tmr
= setInterval(this.update
, 1000);
1443 this.stop = function () {
1444 // Stop the timer and clear the title
1445 if (tmr
) clearInterval(tmr
);
1449 // Some browsers don't always update the last title correctly
1450 // Wait a few seconds and then reset
1451 setTimeout(this.reset
, 2000);
1454 this.reset = function () {
1460 this.update = function () {
1462 that
.setTitle(title
);
1470 $(window
).focus(function (event
) {
1474 // Some browsers don't always update the last title correctly
1475 // Wait a few seconds and then reset
1476 setTimeout(that
.reset
, 2000);
1479 $(window
).blur(function (event
) {
1485 this.alertWindowTimer
.start(title
);
1489 barsHide: function (instant
) {
1493 $('#toolbar').slideUp({queue
: false, duration
: 400, step
: $.proxy(this.doLayout
, this)});
1494 $('#controlbox').slideUp({queue
: false, duration
: 400, step
: $.proxy(this.doLayout
, this)});
1496 $('#toolbar').slideUp(0);
1497 $('#controlbox').slideUp(0);
1502 barsShow: function (instant
) {
1506 $('#toolbar').slideDown({queue
: false, duration
: 400, step
: $.proxy(this.doLayout
, this)});
1507 $('#controlbox').slideDown({queue
: false, duration
: 400, step
: $.proxy(this.doLayout
, this)});
1509 $('#toolbar').slideDown(0);
1510 $('#controlbox').slideDown(0);
1516 initSound: function () {
1518 base_path
= this.model
.get('base_path');
1520 $script(base_path
+ '/assets/libs/soundmanager2/soundmanager2-nodebug-jsmin.js', function() {
1521 if (typeof soundManager
=== 'undefined')
1524 soundManager
.setup({
1525 url
: base_path
+ '/assets/libs/soundmanager2/',
1526 flashVersion
: 9, // optional: shiny features (default = 8)// optional: ignore Flash where possible, use 100% HTML5 mode
1529 onready: function() {
1530 that
.sound_object
= soundManager
.createSound({
1532 url
: base_path
+ '/assets/sound/highlight.mp3'
1540 playSound: function (sound_id
) {
1541 if (!this.sound_object
) return;
1543 if (_kiwi
.global
.settings
.get('mute_sounds'))
1546 soundManager
.play(sound_id
);
1558 _kiwi
.view
.MediaMessage
= Backbone
.View
.extend({
1560 'click .media_close': 'close'
1563 initialize: function () {
1564 // Get the URL from the data
1565 this.url
= this.$el
.data('url');
1568 // Close the media content and remove it from display
1569 close: function () {
1571 this.$content
.slideUp('fast', function () {
1572 that
.$content
.remove();
1576 // Open the media content within its wrapper
1578 // Create the content div if we haven't already
1579 if (!this.$content
) {
1580 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>');
1581 this.$content
.find('.content').append(this.mediaTypes
[this.$el
.data('type')].apply(this, []) || 'Not found :(');
1584 // Now show the content if not already
1585 if (!this.$content
.is(':visible')) {
1586 // Hide it first so the slideDown always plays
1587 this.$content
.hide();
1589 // Add the media content and slide it into view
1590 this.$el
.append(this.$content
);
1591 this.$content
.slideDown();
1597 // Generate the media content for each recognised type
1599 twitter: function () {
1600 var tweet_id
= this.$el
.data('tweetid');
1603 $.getJSON('https://api.twitter.com/1/statuses/oembed.json?id=' + tweet_id
+ '&callback=?', function (data
) {
1604 that
.$content
.find('.content').html(data
.html
);
1607 return $('<div>Loading tweet..</div>');
1611 image: function () {
1612 return $('<a href="' + this.url
+ '" target="_blank"><img height="100" src="' + this.url
+ '" /></a>');
1616 reddit: function () {
1618 var matches
= (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(this.url
);
1620 $.getJSON('http://www.' + matches
[0] + '.json?jsonp=?', function (data
) {
1621 console
.log('Loaded reddit data', data
);
1622 var post
= data
[0].data
.children
[0].data
;
1625 // Show a thumbnail if there is one
1626 if (post
.thumbnail
) {
1627 //post.thumbnail = 'http://www.eurotunnel.com/uploadedImages/commercial/back-steps-icon-arrow.png';
1629 // Hide the thumbnail if an over_18 image
1631 thumb
= '<span class="thumbnail_nsfw" onclick="$(this).find(\'p\').remove(); $(this).find(\'img\').css(\'visibility\', \'visible\');">';
1632 thumb
+= '<p style="font-size:0.9em;line-height:1.2em;cursor:pointer;">Show<br />NSFW</p>';
1633 thumb
+= '<img src="' + post
.thumbnail
+ '" class="thumbnail" style="visibility:hidden;" />';
1636 thumb
= '<img src="' + post
.thumbnail
+ '" class="thumbnail" />';
1640 // Build the template string up
1641 var tmpl
= '<div>' + thumb
+ '<b><%- title %></b><br />Posted by <%- author %>. ';
1642 tmpl
+= '<i class="icon-arrow-up"></i> <%- ups %> <i class="icon-arrow-down"></i> <%- downs %><br />';
1643 tmpl
+= '<%- num_comments %> comments made. <a href="http://www.reddit.com<%- permalink %>">View post</a></div>';
1645 that
.$content
.find('.content').html(_
.template(tmpl
, post
));
1648 return $('<div>Loading Reddit thread..</div>');
1654 // Build the closed media HTML from a URL
1655 buildHtml: function (url
) {
1656 var html
= '', matches
;
1659 if (url
.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
1660 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>';
1664 matches
= (/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/ig).exec(url
);
1666 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>';
1670 matches
= (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(url
);
1672 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>';
1681 _kiwi
.view
.MenuBox
= Backbone
.View
.extend({
1683 'click .ui_menu_foot .close': 'dispose'
1686 initialize: function(title
) {
1689 this.$el
= $('<div class="ui_menu"></div>');
1691 this._title
= title
|| '';
1693 this._display_footer
= true;
1694 this._close_on_blur
= true;
1696 this._close_proxy = function(event
) {
1697 that
.onDocumentClick(event
);
1699 $(document
).on('click', this._close_proxy
);
1703 render: function() {
1706 this.$el
.find('*').remove();
1709 $('<div class="ui_menu_title"></div>')
1711 .appendTo(this.$el
);
1715 _
.each(this._items
, function(item
) {
1716 var $item
= $('<div class="ui_menu_content hover"></div>')
1719 that
.$el
.append($item
);
1722 if (this._display_footer
)
1723 this.$el
.append('<div class="ui_menu_foot"><a class="close" onclick="">Close <i class="icon-remove"></i></a></div>');
1727 onDocumentClick: function(event
) {
1728 var $target
= $(event
.target
);
1730 if (!this._close_on_blur
)
1733 // If this is not itself AND we don't contain this element, dispose $el
1734 if ($target
[0] != this.$el
[0] && this.$el
.has($target
).length
=== 0)
1739 dispose: function() {
1740 _
.each(this._items
, function(item
) {
1741 item
.dispose
&& item
.dispose();
1742 item
.remove
&& item
.remove();
1748 $(document
).off('click', this._close_proxy
);
1752 addItem: function(item_name
, $item
) {
1754 if ($item
.is('a')) $item
.addClass('icon-chevron-right');
1755 this._items
[item_name
] = $item
;
1759 removeItem: function(item_name
) {
1760 delete this._items
[item_name
];
1764 showFooter: function(show
) {
1765 this._display_footer
= show
;
1769 closeOnBlur: function(close_it
) {
1770 this._close_on_blur
= close_it
;
1776 this.$el
.appendTo(_kiwi
.app
.view
.$el
);