/************************************************/
/* IPB3 Javascript								*/
/* -------------------------------------------- */
/* ips.tags.js - Adds tokenizing tags to a textbox */
/* (c) IPS, Inc 2011							*/
/* -------------------------------------------- */
/* Author: Rikki Tissier						*/
/************************************************/

var Tags = {};

Element.addMethods( {
	tagify: function( element, options )
	{
		options = Object.extend( {
			existingTags: []
		}, options);
		
		if( !options.isEnabled ){ return; }
		
		if( options.isOpenSystem ){
			new Tags.Open( element, options );
		} else {
			new Tags.Closed( element, options );
		}		
	}
});

//==================================================
// Base class for tag operations
//==================================================
Tags.Base = {
	parentInit: function(){
		// Set up prefixes
		if( this.options.prefixesEnabled && $( this.id + '_prefix' ) ){
			this.setUpPrefixes();
		}
		
		// Store a reference to this on the tag box
		this.element.store('obj', this);
	},
	
	setUpPrefixes: function(){
		var prefixCheck = $( this.id + '_prefix' );
		
		var prefixFunc = function(e){
			if( $(prefixCheck).checked ){
				$( this.tagWrapper ).addClassName('with_prefixes');
			} else {
				$( this.tagWrapper ).removeClassName('with_prefixes');
			}
		}.bind(this);
		
		prefixFunc();
		
		$( prefixCheck ).observe('click', prefixFunc);
	},
	
	importTags: function(){
		
		if( this.options.existingTags && this.options.existingTags.size() ){
			$A(this.options.existingTags).each( function(item){
				if( !item.blank() )
				{
					this.addTag( item );
				}
			}.bind(this));
		}
	},
	
	buildWrapper: function(){
		// The original textbox, which we'll remember
		this.oldInput = $(this.element).observe('focus', function(e){ this.newInput.focus() }.bind(this))
										.observe('blur', function(e){ this.newInput.blur() }.bind(this));

		// The wrapper which holds our tags
		this.tagWrapper = new Element("ul", { 'id': this.id + '_wrapper' })
									.addClassName("ipsTagBox_wrapper")
									.observe('click', this.eventClickWrapper.bindAsEventListener(this));

		this.oldInput.insert({ 'before': this.tagWrapper }).hide();

		// Create an item for the invisible textbox
		this.inputItem = new Element("li", { 'id': this.id + '_inputItem' }).insert( this.newInput );
		this.tagWrapper.insert({ 'bottom': this.inputItem });
	},
	
	addTag: function( item ){
		item = item.strip();
		
		if( parseInt(this.options.forceLowercase) ){
			item = item.toLowerCase();
		}
		
		var closeTag = new Element("span")
							.addClassName('ipsTagBox_closetag')
							.update("&times;")
							.observe('click', function(e){
								this.deleteTag( Event.findElement(e, 'li') );
							}.bind(this));
		var newTag = new Element("li")
						.writeAttribute("tValue", item)
						.addClassName('ipsTagBox_tag')
						.update( item )
						.insert({'bottom': closeTag});
						
		this.inputItem.insert( { 'before': newTag } );
		
		// Add to hidden input
		this.oldInput.value += item+',';
		
		if( Object.isFunction( this._afterAdd ) ){
			this._afterAdd(item);
		}
	},
	
	selectTag: function( tag ){
		this.deselectAll();
		$( tag ).addClassName('selected');
		this.selectedTag = $( tag );
		
		if( Object.isFunction( this._afterSelect ) ){
			this._afterSelect(tag);
		}
	},
	
	deleteTag: function( tag ){
		
		if( this.selectedTag == $(tag) ){
			this.selectedTag = null;
		}
		
		var value = $(tag).readAttribute('tvalue');
		$(tag).remove();
		
		// Add to hidden input
		this.oldInput.value = this.oldInput.value.replace(value+',', '');
		
		if( Object.isFunction( this._afterDelete ) ){
			this._afterDelete( value );
		}
	},
	
	deselectAll: function(){
		$( this.tagWrapper ).select("li").invoke("removeClassName", 'selected');
		this.selectedTag = null;
	},
	
	calculateTotal: function(){
		return $( this.tagWrapper ).select("li:not(#"+this.id+"_inputItem)").size();
	},
	
	eventClickWrapper: function(e){
		var elem = Event.findElement(e, 'li');
		if( elem && $(elem) != $(this.id + '_inputItem') && $(elem).hasClassName('ipsTagBox_tag') ){
			this.selectTag( $(elem) );
		} else {
			$( this.newInput ).focus();
		}
		return false;
	},

	//--------------------------------------------------------------
	// Public Methods
	//--------------------------------------------------------------
	
	getTagCount: function(){
		return this.calculateTotal();
	},
	
	serializeTags: function(){
		var tags = $A();
		$( this.tagWrapper ).select("li:not(#"+this.id+"_inputItem)").each(function(elem){
			tags.push( $(elem).readAttribute('tvalue') );
		});
		return tags;
	}
}

//==================================================
// 'Closed' tagging (i.e. predefined tags)
//==================================================
Tags.Closed = Class.create( Object.extend( {
	
	initialize: function( element, options ){
		this.element = $( element );
		this.id = this.element.id;
		this.options = options;
		this.handlers = {};
		
		if( Object.isUndefined( this.options.predefinedTags ) || !this.options.predefinedTags || !this.options.predefinedTags.size() ){
			Debug.write( "No predefined tags available" );
			try {
				$( this.element ).up('.tag_field').hide();
			} catch(err){ }
			return;
		}
		
		this.newInput = new Element("a", { 'href': '#' })
									.update( this.options.lang['tag_add_link'] )
									.addClassName("ipsTagBox_addlink")
									.observe('click', this.eventAddNewTag.bindAsEventListener(this));
									
		// Finish building wrapper
		this.buildWrapper();
		
		// Build options dropdown
		this.tagDropdown = new Element("div", { 'id': this.id + '_tagdropdown' })
									.addClassName("ipsTagBox_dropdown")
									.setStyle({
										'width': $(this.tagWrapper).measure('padding-box-width') + 'px',
										'position': 'absolute'
									})
									.hide();
		
		this.tagDropdown.insert( {'bottom' : new Element('ul') } );
		this.options.predefinedTags.each( function(item){
			$( this.tagDropdown ).down('ul').insert({'bottom':	new Element('li')
														.update(item)
														.addClassName("ipsTagBox_dditem")
														.writeAttribute("tvalue", item)
														.observe('click', this.eventClickItem.bindAsEventListener(this))
										});
										
			if ( ! Object.isUndefined( this.options.existingTags ) && this.options.existingTags && this.options.existingTags.size() > 0 )
			{ 
				this.options.existingTags.each( function(existing)
				{
					if ( item == existing )
					{
						$( this.tagDropdown ).select('li[tvalue="'+item+'"]')[0].hide();
					}
				}.bind(this));
			}
		}.bind(this));
		$(this.tagWrapper).insert({'after': this.tagDropdown});
		
		this.importTags();
		
		// Do global init
		this.parentInit();
		
		/* Is this a touch device? */
		if ( ipb.vars['is_touch'] && ! Object.isUndefined( 'iScroll' ) )
		{
			this.myScroll = new iScroll( this.tagDropdown.id, { hideScrollbar: false, hScroll: true } );
		}
	},
	
	showDropdown: function(){
		this.handlers['doc_click'] = $(document.body).on('click', function(e){
			if( !$(e.target).descendantOf( $( this.tagDropdown ) ) && !$(e.target).descendantOf( $( this.tagWrapper ) ) ){
				this.hideDropdown();
			}
		}.bindAsEventListener(this));
		
		this.handlers['doc_key'] =	$(document).on('keypress', function(e){
			if( e.keyCode == Event.KEY_ESC ){
				this.hideDropdown();
			}
		}.bindAsEventListener(this));
		
		new Effect.Appear( $( this.tagDropdown ), { duration: 0.2 } );
		
		/* Is this a touch device? */
		if ( ipb.vars['is_touch'] && ! Object.isUndefined( 'iScroll' ) )
		{
			setTimeout(function () { this.myScroll.refresh(); }.bind(this), 300);
		}
	},
	
	hideDropdown: function(){
		try {
			this.handlers['doc_click'].stop();
			this.handlers['doc_key'].stop();
		} catch(err){ }
		
		new Effect.Fade( $( this.tagDropdown ), { duration: 0.3 } );
		
		if ( $( this.tagDropdown ).select('li').findAll(function(el) { return el.visible(); }).size() )
		{
			if ( ! this.options.maxTags )
			{
				this.newInput.show();
			}
			else if( this.options.maxTags && this.calculateTotal() < this.options.maxTags )
			{
				this.newInput.show();
			}
		}
	},
	
	//--------------------------------------------------------------
	// Event Handlers
	//--------------------------------------------------------------
	
	eventAddNewTag: function(e){
		this.newInput.hide();
		this.showDropdown();
		
		Event.stop(e);
		return false;
	},
	
	eventClickItem: function(e){
		var elem = Event.findElement(e, 'li');
		if( !elem.readAttribute('tvalue') ){ return; }
		var tagName = elem.readAttribute('tvalue');
		this.addTag( tagName );
		
		$(elem).hide();	
		
		// See if all options are used
		if( !$( this.tagDropdown ).select('li').findAll(function(el) { return el.visible(); }).size() ){
			this.hideDropdown();
		}
	},
	
	//--------------------------------------------------------------
	// Internal callbacks
	//--------------------------------------------------------------
	
	_afterSelect: function(tag){
		new Effect.Fade( $(this.tagDropdown), { duration: 0.3 } );
		
		if( this.options.maxTags && this.calculateTotal() < this.options.maxTags ){
			this.newInput.show();
		}
	},
	
	_afterAdd: function(item){
		if( this.options.maxTags && this.calculateTotal() >= this.options.maxTags ){
			this.tagDropdown.hide();
			this.newInput.hide();
		}
	},
	
	_afterDelete: function(value){
		if( ( ! this.options.maxTags ) || ( this.options.maxTags && this.calculateTotal() < this.options.maxTags ) ){
			$( this.tagDropdown ).select('li[tvalue="'+value+'"]')[0].show();
		}
	}
}
, Tags.Base));

//==================================================
// 'Open' tagging class (i.e. free choice)
//==================================================
Tags.Open = Class.create( Object.extend( {
	
	initialize: function( element, options ){
		this.element = $( element );
		this.id = this.element.id;
		this.options = options;
		this.handlers = {};
		
		// Create new textbox which will be our typable area
		this.newInput = new Element("input", { 'type': 'text', 'autocomplete': 'off'})
									.addClassName("ipsTagBox_hiddeninput")
									// Bug #34191 - IE9 would submit the page when enter is pressed. Changed to keydown event.
									.observe('keydown', this.eventKeyPress.bindAsEventListener(this))
									.observe('keyup', this.eventCommaPress.bindAsEventListener(this))
									.observe('blur', this.eventBlurTextbox.bindAsEventListener(this));
									
									/*.observe('focus', this.eventFocusTextbox.bindAsEventListener(this))*/

		this.buildWrapper();
		this.newInput.clear();
		this.importTags();
		
		// Do global init
		this.parentInit();
		
		// Set instructional text if there's no entries
		if( !this.calculateTotal() && !$F( this.newInput )){
			$( this.newInput ).addClassName('inactive').value = this.options.lang['tip_text'];
			
			this.handlers['text_focus'] = $( this.newInput ).on('focus', function(e){
				$( this.newInput ).removeClassName('inactive').value = "";
				if( this.options.maxLen ){
					this.newInput.writeAttribute("maxlength", this.options.maxLen - 1); // -1 to leave room for comma
				}
				this.handlers['text_focus'].stop();
			}.bindAsEventListener(this));
		}
	},
	
	//--------------------------------------------------------------
	// Event Handlers
	//--------------------------------------------------------------
	
	eventKeyPress: function(e){	
		//Debug.write( e.keyCode );
		switch( e.keyCode ){
			case Event.KEY_BACKSPACE:
				//alert( this.inputItem.previous().tagName );
				if( !$F(this.newInput) )
				{
					if( this.selectedTag ){
						this.deleteTag( this.selectedTag );
					}
					else
					{
						if( this.inputItem.previous() ){
							this.selectTag( this.inputItem.previous() );
						}
					}
				}				
			break;
			case Event.KEY_DELETE:
				if( this.selectedTag ){
					this.deleteTag( this.selectedTag );
				}
			break;	
			case Event.KEY_RETURN:
			case Event.KEY_TAB:			
				// Bug #30286 - arabic/hebrew keyboards have a different character on
				// key 188. We have to check that the last character is really a latin
				// comma, and ignore it if not.
				//---------------
				// Bug #33967 - typing tags fast caused them to mess up. Instead of checking
				// if the last char is a comma, we now just check whether a comma exists in
				// the string at all.				
				if( (e.keyCode == Event.KEY_TAB) && this.newInput.value == '' )
				{
					return false;
				}
				
				Event.stop(e);
								
				var value = this._stripHtml( this.newInput.value.replace(/\,/, '') );
				if( !value ){
					this.newInput.value = "";
					return false;
				}
								
				this.addTag( value );
				this.newInput.value = "";
			break;
		}
	},
	
	// Separate event for comma press - event is keyup, not keydown, so that
	// international keyboard that share the comma key still work.
	eventCommaPress: function(e){
		// 188 = comma
		if( e.keyCode != 188 ){
			return;
		}

		// Bug36871 - RTL issue is back.  Previous fix does not account for it apparently.
		// Just going to ignore comma presses when isRTL is true.
		if( isRTL )
		{
			return false;
		}
		
		if( !this.newInput.value.indexOf(",") ){
			return;
		}
		
		Event.stop(e);
						
		var value = this._stripHtml( this.newInput.value.replace(/\,/, '') );
		if( !value ){
			this.newInput.value = "";
			return false;
		}
						
		this.addTag( value );
		this.newInput.value = "";
	},
	 
	eventBlurTextbox: function(e){
		if( this.newInput.value != '' && this.newInput.value != ',' )
		{
			var value = this._stripHtml( this.newInput.value.replace(/\,/, '') );
			this.addTag( value );
			this.newInput.value = "";
		}
	},
	
	_stripHtml: function( value )
	{
		var remove = [ '<', '>', '"', "'" ];
		
		remove.each( function(item)
		{
			value = value.replace( new RegExp( item, 'g' ), '' );
		} );
		
		return value;
	},
	
	//--------------------------------------------------------------
	// Internal callbacks
	//--------------------------------------------------------------
	
	_afterSelect: function(tag){
		$( this.newInput ).setValue("").focus();
	},
	
	_afterAdd: function(item){
		if( this.options.maxTags && this.calculateTotal() >= this.options.maxTags ){
			this.newInput.hide();
		}
	},
	
	_afterDelete: function(tag){
		if( this.options.maxTags && this.calculateTotal() < this.options.maxTags ){
			this.newInput.show().focus();
		}
	}
}
, Tags.Base));
