Ext.ns('Ext.ux.RapidApp.AppDV');


Ext.ux.RapidApp.AppDV.DataView = Ext.extend(Ext.DataView, {
	
  // New option to enable all links (<a> tags)to be processed. This is not the default
  // because this can result is navigating away from the interface which is
  // usually not desired. Links with explicit target attributes (i.e. target="_blank")
  // are already handled, and relative links are also already handled. The main
  // reason for this setting and the fact that it is false by default is due both
  // to back-compat, and also the fact that the native Ext.DataView code does not allow
  // links through by default when singleSelect or multiSelect is on
  allow_all_links: false,
  
	// TODO: make cascade recursive like in Ext.Container
	cascade: function(fn,scope,args) {
		fn.apply(scope || this, args || [this]);
		return this;
	},
	
  // collectData() is called whenever rendering/refreshing the template:
  collectData: function() {
    // New: Save the latest response data within the XTemplate object via the
    // reference keys which are now made available via DataStorePlus. This
    // enables the template environment to get this data via '[{this.resData}]'
    try{
      var hashval = window.location.hash;
      if(hashval && hashval.search('#') == 0) { 
        hashval = hashval.substring(1); 
      }
      this.tpl.hashval = hashval;
      this.tpl.resData = this.store.lastJsonData
        // The lastJsonData might not be populated on the first load, for this 
        // case,   // reach into the lastResponse, which is also now tracked, and decode it
        || Ext.decode(this.store.proxy.lastResponse.responseText);
    }catch(err){};
    return Ext.ux.RapidApp.AppDV.DataView.superclass.collectData.apply(this,arguments);
  },
  
  getLoadMaskEl: function() {
    var El = this.getEl();
    return El ? El.parent().parent() : Ext.getBody();
  },
  
	initComponent: function(){
		Ext.each(this.items,function(item) {
			item.ownerCt = this;
		},this);
		Ext.ux.RapidApp.AppDV.DataView.superclass.initComponent.call(this);
		this.components = [];
    
    this.on('beforeclick',this.onBeforeclick,this);
		this.on('click',this.click_controller,this);
		
    this.on('containerclick',function(dv,event){
      // Block the containerclick, which clears selections, for the special 
      // AppDV-specific clickable command (i.e. store buttons, etc)
      if(this.find_clickableEl(event)){ return false; };
    },this);
    
    this.tpl.store = this.store;
    
		//if(!this.store) { this.store = this.ownerCt.store; }
		
		this.store.on('beforesave',this.onBeforesave,this);
		this.store.on('beforeremove',this.onBeforeremove,this);
		
		// Special AppDV override: addNotAllowed based on
		// current edit record:
		var cmp = this;
		cmp.on('afterrender',function(){
      this.el.on('click',this.el_click_controller,this);
 
			cmp.store.addNotAllowed = function(){
				if(cmp.currentEditRecord && cmp.currentEditRecord.editing) {
					return true;
				}
				return false;
			}
			/* TODO:
			if(!cmp.store.hasPendingChangesOrig) {
				cmp.store.hasPendingChangesOrig = cmp.store.hasPendingChanges;
			}
			cmp.store.hasPendingChanges = function() {
				//console.log('has pending changes');
				if(cmp.store.addNotAllowed()) { return true; }
				return cmp.store.hasPendingChangesOrig.apply(this,arguments);
			};
			*/
		},this);
		
		this.on('beforeselect',this.onBeforeselect,this);
    
    if(this.refresh_on_save) {
      this.store.on('write',function(){
        this.refresh.apply(this,arguments);
      },this);
    }
    
    if(this.init_record_editable) {
      this.on('firstload',function(cmp,ds){
        var node = this.getNode(0,0);
        if(node) {
          var El = new Ext.Element(node);
          var Toggle = El.child('div.edit-record-toggle');
          if(Toggle) {
            Toggle.dom.click();
          }
        }
      },this);
    }
    
    if(this.refresh_on_hash_change) {
      Ext.History.on('change',this.refresh,this);
      this.on('beforedestroy',function(){
        Ext.History.un('change',this.refresh,this);
      },this);
    }
    
    // init
    this.FieldCmp = {};
    
	},
	
	onBeforeselect: function() {
		// We don't want to allow clicks to toggle record select status when
		// we are editing:
		if(this.currentEditRecord && this.currentEditRecord.editing) {
			return false;
		}
	},
	
	onBeforesave: function() {
		this.isSaving = true;
		this.simulateSaveClick.call(this);
		this.isSaving = false;
	},
	
	refresh: function(){
		Ext.destroy(this.components);
		this.components = [];

    //Ext.ux.RapidApp.AppDV.DataView.superclass.refresh.call(this);
    // --- Orig refresh(), with modified 'emptyText' behavior (GitHub Issue #157)---
    this.clearSelections(false, true);
    var el = this.getTemplateTarget(),
        records = this.store.getRange();
        
    el.update('');
    // We do not apply the 'emptyText' if it is empty (new for GH #157):
    if(records.length < 1 && this.emptyText && this.emptyText != ''){
        if(!this.deferEmptyText || this.hasSkippedEmptyText){
            el.update(this.emptyText);
        }
        this.all.clear();
    }else{
        this.tpl.overwrite(el, this.collectData(records, 0));
        this.all.fill(Ext.query(this.itemSelector, el.dom));
        this.updateIndexes(0);
    }
    this.hasSkippedEmptyText = true;
    // ---

    // renderItems relates to the special case of sub-componenets, not 
    // rendering the normal, local records/columns and their editors
    this.renderItems(0, this.store.getCount() - 1);
    
    this.toggleDirtyCssRecord(records,false);
    this.injectDynamicStylesheet();
	},
	
	onUpdate: function(ds, record){
		var index = ds.indexOf(record);
		if(index > -1){
				this.destroyItems(index);
		}
		Ext.ux.RapidApp.AppDV.DataView.superclass.onUpdate.apply(this, arguments);
		if(index > -1){
				this.renderItems(index, index);
		}
		this.toggleDirtyCssRecord(record,true);
	},
	
	onAdd: function(ds, records, index){
		var count = this.all.getCount();
		Ext.ux.RapidApp.AppDV.DataView.superclass.onAdd.apply(this, arguments);
    
    var dsPlug = this.datastore_plus_plugin;
    if(!dsPlug.use_add_form && !this.persist_immediately.create) {
    
      if(count !== 0){
        this.renderItems(index, index + records.length - 1);
      }
      
      var Record;
      //Get first phantom record:
      Ext.each(records,function(rec) {
        if(Record || !rec.phantom) { return; }
        Record = rec;
      },this);
      
      if(Record) {
        this.currentEditRecord = Record;
        var domEl = this.getNode(Record);
        var editEl = new Ext.Element(domEl);
        this.currentEditEl = editEl;
        this.clearSelections();
        this.handle_edit_record(editEl,editEl,Record,index,editEl);
      }
    }
		
    // If scrollNodeClass, this should work:
    if(this.scrollNodeClass) {
      this.scrollRecordIntoView.defer(10,this,[records[records.length - 1]]);
    }
    else {
      this.scrollRecord.defer(10,this,[records[records.length - 1]]);
    }
		this.highlightRecord.defer(10,this,[records]);
		this.toggleDirtyCssRecord(records,true);

	},
	
	forEachRecordNode: function(fn,record) {
		
		if(Ext.isArray(record)){
			Ext.each(record, function(r){
				this.forEachRecordNode(fn,r);
			},this);
			return;
		}
	  
		if(!record || !record.store) { return; }

		var node = this.getNode(record);
		if(!node) { return; }
		var el = new Ext.Element(node);
		fn(el,record);
	},
	
	highlightRecord: function(record) {
		this.forEachRecordNode(function(el){
			el.highlight();
		},record);
	},
  
  scrollRecord: function(record) {
    var dvEl = this.el;
    this.forEachRecordNode(function(el){
      el.scrollIntoView(dvEl);
    },record);
  },
	
	puffRecord: function(record) {
		this.forEachRecordNode(function(el){
			el.fadeOut({
				easing: 'easeNone',
				duration: .5,
				remove: false,
				useDisplay: false,
				concurrent: true
			});
			el.highlight();
		},record);
	},
	
	toggleDirtyCssRecord: function(record,tog) {
		var dv = this;
		this.forEachRecordNode(function(el,rec){
			//if(rec.dirtyEl) { rec.dirtyEl.remove(); }
			if(tog && el && rec.dirty) {
        
        // New: sets the dirty flag on the appropriate, individual fields:
        var fieldDoms = el.query('div.field-name'); 
        Ext.each(fieldDoms,function(fDom) {
          var fname = fDom.innerHTML;
          if(rec.fields.get(fname)) {
            var fEl = new Ext.Element(fDom);
            var dWrap = fEl.parent('div.editable-value');
            if(dWrap) {
              if(dWrap.hasClass('x-grid3-dirty-cell')) {
                dWrap.removeClass('x-grid3-dirty-cell');
              }
              if(typeof rec.modified[fname] !== "undefined") {
                dWrap.addClass('x-grid3-dirty-cell');
              }
            }
          }
        },this);
        
				// This logic sets a dirty flag on the entire record... disabled 
        // after adding the above, field-specific dirty flagging
				//var domCfg = {
				//	tag: 'div',
				//	style: 'position:absolute;',
				//	children:[{
				//		tag: 'div',
				//		cls: 'x-grid3-dirty-cell',
				//		style: 'position:relative;top:0;left:0;z-index:15000;height:10px;width:10px;'
				//	}]
				//};
				//
				//if(el.dom.tagName.toUpperCase() == 'TR') {
				//	domCfg = {
				//		tag: 'tr',
				//		children:[{
				//			tag: 'td',
				//			children:[domCfg]
				//		}]
				//	};
				//}
				//
				//rec.dirtyEl = el.insertSibling(domCfg,'before');
			}
      
      if(rec.phantom) {
        if(el.hasClass('non-phantom')) { el.removeClass('non-phantom'); }
        if(!el.hasClass('is-phantom')) { el.addClass('is-phantom'); }
      }
      else {
        if(el.hasClass('is-phantom')) { el.removeClass('is-phantom'); }
        if(!el.hasClass('non-phantom')) { el.addClass('non-phantom'); }
      }
      
		},record);
	},
	
	onBeforeremove: function(ds, record){
		
		if(this.removeInProgress) { return true; }
		this.toggleDirtyCssRecord(record,false);
		if(record == this.currentEditRecord) {
			var index = this.getStore().indexOf(record);
			this.simulateCancelClick(record,index,this.currentEditEl);
			return false;
		}
		
		this.puffRecord(record);
		
		this.removeInProgress = true;
		var doRemove = function(){
			ds.remove.apply(this,arguments);
			this.removeInProgress = false;
      // we have to do this ourselves because we cancel the default remove event,
      // normally this would be done for us by the DataStorePlus plugin
      if(this.datastore_plus_plugin.persist_immediately.destroy) { 
        ds.saveIfPersist();
      }
		};
		doRemove.defer(300,this,[record]);
		
		return false;
	},
	onRemove: function(ds, record, index){
		
		this.destroyItems(index);
		Ext.ux.RapidApp.AppDV.DataView.superclass.onRemove.apply(this, arguments);
	},
	
	onDestroy: function(){
		Ext.ux.RapidApp.AppDV.DataView.superclass.onDestroy.call(this);
		Ext.destroy(this.components);
		this.components = [];
	},
	
	renderItems: function(startIndex, endIndex){
		var ns = this.all.elements;
		var args = [startIndex, 0];
		
		//console.dir(args);
		
		for(var i = startIndex; i <= endIndex; i++){
			var r = args[args.length] = [];
			for(var items = this.items, j = 0, len = items.length, c; j < len; j++){
			
				// c = items[j].render ?
				//	c = items[j].cloneConfig() :
					
				// RapidApp specific:
				// Components are stored as serialized JSON to ensure they
				// come out exactly the same every time:
				var itemCnf = Ext.decode(items[j]);
				itemCnf.ownerCt = this;

				// renderDynTarget will look for a child div with class="encoded-params" containing
				// JSON encoded additional params that will be dynamically applied to the
				// config of the component being created. Essentially this allows part or all
				// of the component config to be stored directly within the HTML markup. Typically
				// the encoded-params div will have style="display:none;" to prevent the JSON
				// from showing up on the page.
				if(itemCnf.renderDynTarget) {
					var Node = Ext.DomQuery.selectNode(itemCnf.renderDynTarget, ns[i]);
					if(Node) {

						var cnf = {};
						Ext.apply(cnf,itemCnf);

						var encNode = Ext.DomQuery.selectNode('div.encoded-params', Node);
						if(encNode) {
							Ext.apply(cnf,Ext.decode(encNode.innerHTML));
						}
						
						c = Ext.create(cnf, this.defaultType);
						r[j] = c;
						c.render(Node);
					}
				}
				else {
					c = Ext.create(itemCnf, this.defaultType);
					r[j] = c;
					
					if(c.renderTarget){
						c.render(Ext.DomQuery.selectNode(c.renderTarget, ns[i]));
					}
					else if(c.applyTarget){
						c.applyToMarkup(Ext.DomQuery.selectNode(c.applyTarget, ns[i]));
					}
					else{
						c.render(ns[i]);
					}
				}	
				
				if(c && Ext.isFunction(c.setValue) && c.applyValue){
					c.setValue(this.store.getAt(i).get(c.applyValue));
					c.on(
						'blur', 
						function(f){
							this.store.getAt(this.index).data[this.dataIndex] = f.getValue();
						},
						{store: this.store, index: i, dataIndex: c.applyValue}
					);
				}

			}
		}
		this.components.splice.apply(this.components, args);
    
    // re-scan for ra-async-box elements -- codepath is specific to AppDV
    Ext.ux.RapidApp.loadAsyncBoxes(this);
	},
	
	destroyItems: function(index){
		Ext.destroy(this.components[index]);
		this.components.splice(index, 1);
	},
	
	get_new_record: function(initData) {
		
		var Store = this.getStore();
		
		// abort if the Store doesn't have create in its API:
		if(!Store.api.create) { return false; } 
		
		var node = this.getNode(0);
		if(node) {
			var nodeEl = new Ext.Element(node);
			// abort if another record is already being updated:
			if(nodeEl.parent().hasClass('record-update')) { return; }
		}
		
		var recMaker = Ext.data.Record.create(Store.fields.items);
		var newRec;
		if(initData){
			newRec = new recMaker(initData);
		}
		else {
			newRec = new recMaker;
		}
		
		if(! Store.api.create) {
			Ext.Msg.alert('Cannot add','No create function has been defined');
			return false;
		}
		return newRec;
	},
	
	add_record: function(initData) {
		var newRec = this.get_new_record(initData);
		if (newRec) {
			return this.getStore().add(newRec);
		}
	},
	
	insert_record: function(initData) {
		var newRec = this.get_new_record(initData);
		if (newRec) {
			return this.getStore().insert(0,newRec);
		}
	},
	
	
	
	set_field_editable: function(editEl,fieldname,index,Record,domEl) {
		
		//abort if its already editing:
		if(editEl.hasClass('editing')) { return; }
		
		var dataWrap = editEl.child('div.data-wrapper');
		var dataEl = editEl.child('div.data-holder');
		var fieldEl = editEl.child('div.field-holder');
    
    if(!this.fieldRecordCanEdit(fieldname,Record)) { return; }

		
		editEl.addClass('editing');

		var cnf = {};
		Ext.apply(cnf,Ext.decode(this.FieldCmp_cnf[fieldname]));
		Ext.apply(cnf,{
			ownerCt: this,
			Record: Record,
			renderTo: fieldEl
			//contentEl: dataEl
		});
    
    if(Record && Record.data && typeof Record.data[fieldname] !== "undefined") {
      // This will not get called in add record context, and we don't want it to:
      cnf.value = Record.data[fieldname];
    }
    
		
		if(!cnf.width) {	cnf.width = dataEl.getWidth() + 10; }
		if(!cnf.height) { cnf.height = dataEl.getHeight() + 6; }
		if(cnf.minWidth) { if(!cnf.width || cnf.width < cnf.minWidth) { cnf.width = cnf.minWidth; } }
		if(cnf.minHeight) { if(!cnf.height || cnf.height < cnf.minHeight) { cnf.height = cnf.minHeight; } }
		
		// UPDATE: using visibility mode across the board now because the other method was
		// causing images to overlap in some cases (2011-10-10 by HV)
		//if(Ext.isIE) {
			dataEl.setVisibilityMode(Ext.Element.DISPLAY);
			dataEl.setVisible(false);
		//}
		//else {
			// Stupid IE can't do it with contentEl, but we want to do the contentEl
			// way because if we use the hide method the element jumps in an
			// ungly way in FF.
		//	cnf.contentEl = dataEl;
		//}
		
		var Store = this.getStore();
    
    cnf.AppDv_context = {
      appdv: this,
      Record: Record,
      editEl:editEl,
      index: index
    };
    cnf.plugins = cnf.plugins || [];
    cnf.plugins.push('appdv-field-plugin');
    
    // New: handle the special appdv-fill-absolute case - remove all
    // dynamic sizing options and apply the target rule to set 'absolute'
    // from and according to the CSS
    if(editEl.findParent('div.appdv-fill-absolute')) {
      if(cnf.grow) { cnf.grow = false; }
      cnf.cls = 'appdv-fill-absolute-target-rule';
      //cnf.width = '100%';
      cnf.height = 'auto';
    }
    
		
		var Field = Ext.create(cnf,'field');
    
    Field.reportDirtyDisplayVal = function(disp) {
      Record._dirty_display_data = Record._dirty_display_data || {};
      Record._dirty_display_data[Field.name] = disp;
    }
		
		if(!Ext.isObject(this.FieldCmp)) { this.FieldCmp = {} }
		if(!Ext.isObject(this.FieldCmp[index])) { this.FieldCmp[index] = {} }
		this.FieldCmp[index][fieldname] = Field;
		
		
		/*****************************************************/
		// don't do this if the entire record is in edit mode or another record is already being updated:
		if(domEl &&(!domEl.hasClass('editing-record') && !domEl.parent().hasClass('record-update'))) { 

			var s = this.currentEditingFieldScope;
			if(s) {
				// cancel editing of any other field already being edited
				this.cancel_field_editable(s.editEl,s.fieldname,s.index,s.Record);
			}
			
			s = {
				editEl: editEl,
				fieldname: fieldname,
				index: index,
				Record: Record
			};
			
			var endEdit = function() {
				this.cancel_field_editable(editEl,fieldname,index,Record);
			};
			
			var saveEndEdit = function() {
				//console.log('saveEndEdit');
				this.save_field_data(editEl,fieldname,index,Record);
				Store.saveIfPersist();
				endEdit.call(this);
			};
			
			this.currentEditingFieldScope = s;
			
			// Setup keymaps for Enter and Esc:
			Field.on('specialkey',function(field,e) {
				if(e.getKey() == e.ENTER) {
          // triggerBlur is on trigger/combos, and needs to be called to call any needed
          // logic to apply the raw field value 
          if(Ext.isFunction(field.triggerBlur)) {
            field.triggerBlur();
          }
					if(! field.isValid()) { return; }
					saveEndEdit.call(this);
				}
				else if(e.getKey() == e.ESC) {
					endEdit.call(this);
				}
			},this);
			
			// If its a combo then set/save on select
			Field.on('select',function(field) {
				//console.log('AppDV select');
				
				if(field && ! field.isValid()) { return; }
				saveEndEdit.call(this);
			},this);
			
			if(Ext.isFunction(Field.selectText)) {
				// Focus the field and put the cursor at the end
				Field.on('show',function(field){
					field.focus();
					field.setCursorPosition(1000000);
				},this);
			}
			
      // Fire the trigger if present (i.e. expand dropdown)
      if(Ext.isFunction(Field.onTriggerClick)) {
        Field.on('show',function(field){
          field.onTriggerClick();
        },this);
      }

		}
		/*****************************************************/
		
		// This logic moved into Ext.ux.RapidApp.HtmlEditor
		//if(Field.resizable) {
		//	var resizer = new Ext.Resizable(Field.wrap, {
		//		pinned: true,
		//		handles: 's',
		//		//handles: 's,e,se',
		//		dynamic: true,
		//		listeners : {
		//			'resize' : function(resizable, height, width) {
		//				Field.setSize(height,width);
		//			}
		//		}
		//	});
		//}
		
		Field.show();
		
	},
	
	save_field_data: function(editEl,fieldname,index,Record) {
		if(!editEl.hasClass('editing')) { return false; }
		var Field = this.FieldCmp[index][fieldname];
			
		if(!Field.validate()) { return false; }
    if(Field.isDirty()) {
      var val = Field.getValue();
      Record.set(fieldname,val);
    }
		
		return true;
	},
	
	cancel_field_editable: function(editEl,fieldname,index,Record) {
	
		var dataWrap = editEl.child('div.data-wrapper');
		var dataEl = editEl.child('div.data-holder');
		var fieldEl = editEl.child('div.field-holder');
		
    
    if(this.FieldCmp[index] && this.FieldCmp[index][fieldname] && dataWrap && dataEl && fieldEl) {
			var Fld = this.FieldCmp[index][fieldname];
			if(Fld.contentEl) {
				Fld.contentEl.appendTo(dataWrap);
			}
      // remove the field valid state from consideration (will clear if the field was invalid)
      this.fireEvent('valid',Fld); 
			Fld.destroy();
			dataEl.setVisible(true);
			
			editEl.removeClass('editing');
		}
		delete this.currentEditingFieldScope;
	},
  
  find_clickableEl: function(event,domNode) {
    var target = event.getTarget(null,null,true);
    
    // --- Override nav links
    var href = target.getAttribute('href');
    if(href && target.is('a')) {
      // New: ignore links with target attribute (i.e. target="_self", etc)
      if(target.getAttribute('target')) {
        return null;
      }
      // HashNav links (a tags with href starting with '#!/'):
      else if(href.search('#!/') === 0) {
        window.location.hash = href;
        return null;
      }
    }
    // ---
      
    // Limit processing to click nodes within this dataview (i.e. not in our submodules)
    var topmostEl = target.findParent('div.appdv-tt-generated.' + this.id,null,true);
    if(!topmostEl) { 
      // Temporary: map to old function:
      //return Ext.ux.RapidApp.AppDV.click_handler.apply(this,arguments);
      return null; 
    }
    var clickableEl = topmostEl.child('div.clickable');
  
    return clickableEl;
  },

  // The el_click_controller handles raw clicks on the whole content area, not just
  // a specific record, like the click_controller
  el_click_controller: function(event,domNode,o) {
    var clickableEl = this.find_clickableEl(event,domNode);
    
    // We only handle class="clickable command" (click_controller handles class="clickable")
    if(clickableEl && clickableEl.hasClass('command')) {
      
      var cmdEl = clickableEl.child('div.store-button');
      if(cmdEl) {
        //this.store.addRecordForm();
          
        var Btn, dsPlug = this.datastore_plus_plugin;
        Ext.each(dsPlug.store_buttons,function(itm){
          if(cmdEl.hasClass(itm)) {
            Btn = dsPlug.getStoreButton(itm);
            return false; //<-- stop iteration
          }
        },this);
        
        if(Btn && !Btn.disabled && Btn.handler) {
          Btn.handler.call(this,Btn);
        
        }
        
        //console.dir(Btn);
      
        return;
      }
      
      // handle other commands ...
    
    }
  },
  
    
  onBeforeclick: function(dv, index, domNode, event) {
  
    // Important: when multiSelect or singleSelect is enabled, if we don't return 
    // *false* from this event to stop it, the native Ext.DataView code will block
    // the ordinary browser event (by calling e.preventDefault()). This will stop
    // ordinary links from working. So, if we want links to work, we have to handle
    // here, manually:
  
    // testTarget()
    // Returns true if the supplied target should be handled by AppDV, false if
    // it should be handled natively by the browser
    var testTarget;
    testTarget = function(target) {
      // We're still not going to allow *ALL* links through... We are letting
      // through links with the special 'filelink' class, and also letting 
      // through links which have defined a target (i.e. target="_blank", etc),
      // otherwise we will continue with the existing native behavior which is
      // to make the link do nothing in the browser, but be handled in AppDV. 
      // Also, note that we are now enabling the
      // 'ra-link-click-catcher' in AppDV per default (see AppDV.pm) so it may
      // still pickup and handle relative URL links in the standard manner.
      if(
        target.hasClass('filelink') ||
        target.getAttribute('target') || 
        // New: if this is a form submit button/input, assume the user means it
        // to submit as usual and allow it through:
        (target.getAttribute('type') && target.getAttribute('type')  == 'submit')
      ) {
        // If we're here it means the target matches one of our exclusions, so we'll
        // return false to allow the browser to handle it **unless** it is within a
        // declared, appdv clickable element. We always want to handle these ourselves
        var click_parent = target.parent('div.clickable');
        if(click_parent && click_parent.parent('div.appdv-tt-generated')){
          return true;
        }
        else {
          return false; 
        }
      }
      
      if(target.is('a')) {
        if(this.allow_all_links) {
          return false;
        }
      }
      else {
        // If we're not an <a> tag, check to see if we are within an <a> tag, and
        // for that case consider the parent <a> tag:
        var parent = target.parent('a');
        if(parent) {
          return testTarget(parent);
        }
      }
      
      return true;
    };
    
    return testTarget( event.getTarget(null,null,true) );
  },
  
	click_controller: function(dv, index, domNode, event) {
    var clickableEl = this.find_clickableEl.call(dv,event,domNode);
    
    if(!clickableEl) { return; }
    
    var target = event.getTarget(null,null,true);
    var domEl = new Ext.Element(domNode);

		var Store = this.getStore();
		var Record = Store.getAt(index);
		
		var editEl = clickableEl.child('div.editable-value');
		if(editEl) {
			
			// abort if the Store doesn't have update in its API:
			if(!Store.api.update) { return; } 
			return this.handle_edit_field(target,editEl,Record,index,domEl);
		}
		
		editEl = clickableEl.child('div.edit-record-toggle');
		if(editEl) {
			// abort if the Store doesn't have update in its API and we're not already
			// in edit mode from an Add operation:
			if(!Store.api.update && !domEl.hasClass('editing-record')) { return; } 
			return this.handle_edit_record(target,editEl,Record,index,domEl);
		}
		
		editEl = clickableEl.child('div.delete-record');
		if(editEl) {
			// abort if the Store doesn't have destroy in its API:
			if(!Store.api.destroy) { return; } 
			return this.handle_delete_record(target,editEl,Record,index,domEl);
		}
		editEl = clickableEl.child('div.print-view');
		if(editEl) {
			if(this.printview_url) {
				window.open(this.printview_url,'');
			}
		}
	},

	get_fieldname_by_editEl: function(editEl) {
		var fieldnameEl = editEl.child('div.field-name');
		if(!fieldnameEl) { return false; }
		
		return fieldnameEl.dom.innerHTML;
	},
	handle_delete_record: function (target,editEl,Record,index,domEl) {
		
		// abort if the entire record is in edit mode:
		if(domEl.hasClass('editing-record')) { return; }
		
		// abort if another record is already being updated:
		if(domEl.parent().hasClass('record-update')) { return; }
		
		var Store = this.getStore();
		
		Store.removeRecord(Record);
		//if (!Record.phantom) { Store.saveIfPersist(); }
	},
	handle_edit_field: function (target,editEl,Record,index,domEl) {
		
		// abort if the entire record is in edit mode:
		if(domEl.hasClass('editing-record')) { return; }
		
		// abort if another record is already being updated:
		if(domEl.parent().hasClass('record-update')) { return; }
		
		var Store = this.getStore();
		
		var fieldname = this.get_fieldname_by_editEl(editEl);
		if(!this.fieldRecordCanEdit(fieldname,Record)) { return; }
		
		var dataWrap = editEl.child('div.data-wrapper');
		var dataEl = editEl.child('div.data-holder');
		var fieldEl = editEl.child('div.field-holder');

		if (editEl.hasClass('editing')) {
		
			var Field = this.FieldCmp[index][fieldname];
			
			if(target.hasClass('save')) {
				if(!this.save_field_data(editEl,fieldname,index,Record)) { return; }
				//Store.save();
				Store.saveIfPersist();
			}
			else {
				if(!target.hasClass('cancel')) { return; }
			}
		
			this.cancel_field_editable(editEl,fieldname,index,Record);
		}
		else {
			// require-edit-click is set by "edit-bigfield" to disallow going into edit mode unless the
			// "edit" element itself was clicked:
			if(target.findParent('div.require-edit-click') && !target.hasClass('edit')) { return; }
			this.set_field_editable(editEl,fieldname,index,Record,domEl);
			
		}
		
	},
	beginEditRecord: function(Record) {
		if(Record.editing) { return; }
		Record.beginEdit();
		this.currentEditRecord = Record;
		var Store = this.getStore();
		Store.fireEvent('buttontoggle',Store);
		this.clearSelections();
	},
	endEditRecord: function(Record) {
		if(!Record.editing) { return; }
		Record.endEdit();
		this.currentEditRecord = null;
		var Store = this.getStore();
		Store.fireEvent('buttontoggle',Store);
	},
	simulateEditRecordClick: function(cls,Record,index,editEl) {
		
		if(!Record) { Record = this.currentEditRecord; }
		if(!Record) { return; }
		
		if(!editEl) {
			var domEl = this.getNode(Record);
			editEl = new Ext.Element(domEl);
		}

		var TargetEl = editEl.child(cls);		
		if(typeof index === 'undefined') { index = this.getStore().indexOf(Record); }

		return this.handle_edit_record(TargetEl,editEl,Record,index,editEl);
	},
	simulateSaveClick: function() {
		return this.simulateEditRecordClick('div.save');
	},
	simulateCancelClick: function(Record,index,editEl) {
		return this.simulateEditRecordClick('div.cancel',Record,index,editEl);
	},
  
  // Returns true/false if the field can edit, properly considering edit vs add
  fieldRecordCanEdit: function(fieldname,Record) {
    if(!fieldname || !Record) { return false; }
    var column = Record.store.getColumnConfig(fieldname);
    if(!column) { return false; }
    if(Record.phantom) {
      if(!column.allow_add) { return false; }
      if(!Record.store.api.create) { return false; }
    }
    else {
      if(!column.allow_edit) { return false; }
      if(!Record.store.api.update) { return false; }
    }
    
    return true;
  },
  
	
	handle_edit_record: function (target,editEl,Record,index,domEl) {
		
		var Store = this.getStore();
		
		// New: use the datastore-plus edit record function:
		if(this.use_edit_form && !Record.phantom){
			return Store.editRecordForm(Record);
		}

		var editDoms = domEl.query('div.editable-value');
		var editEls = [];
		Ext.each(editDoms,function(dom) {
			editEls.push(new Ext.Element(dom));
		});
		
		if(domEl.hasClass('editing-record')) {
			
			var save = false;
			if(target.hasClass('save')) {
				save = true;
			}
			else {
				if(!target.hasClass('cancel')) { return; }
			}

			this.beginEditRecord(Record);
		
			var success = true;
			/***** SAVE RECORDS *****/
			Ext.each(editEls,function(editEl) {
				var fieldname = this.get_fieldname_by_editEl(editEl);
				if(!this.fieldRecordCanEdit(fieldname,Record)) { return; }
				
				if(save) {
					if(!this.save_field_data(editEl,fieldname,index,Record)) { 
						success = false;
						return;
					}
				}
			},this);
			
			if(!success) {
				return;
			}
			
			
			/***** REMOVE EDIT STATUS *****/
			Ext.each(editEls,function(editEl) {
				var fieldname = this.get_fieldname_by_editEl(editEl);
				this.cancel_field_editable(editEl,fieldname,index,Record);
			},this);
			
			domEl.removeClass('editing-record');	
			if(domEl.parent()) {
        domEl.parent().removeClass('record-update');
      }
			//Record.endEdit();
			this.endEditRecord(Record);
			
			if(Record.phantom && !save) {
				return Store.remove(Record);
			}
			
			//this.scrollBottomToolbarIntoView.defer(100,this);
			if (this.isSaving) { return; }
			
			// persist_on_add is AppDV specific, and causes a store save to happen *after* a
			// new record has been added via filling out fields. when persist_immediately.create
			// is set empty records are instantly created without giving the user the chance
			// set the initial values
			if(Record.phantom && this.persist_on_add) { return Store.save(); }
			
			return Store.saveIfPersist();
		}
		else {
			// abort if another record is already being updated:
      if(domEl.parent()) {
        if(domEl.parent().hasClass('record-update')) { return; }
        domEl.parent().addClass('record-update');
      }
			domEl.addClass('editing-record');
			
			Ext.each(editEls,function(editEl) {
				var fieldname = this.get_fieldname_by_editEl(editEl);
				this.set_field_editable(editEl,fieldname,index,Record);
			},this);

			this.beginEditRecord(Record);
		}
	},
	
	scrollBottomToolbarIntoView: function(){
		var node = this.getParentScrollNode(this.getEl().dom);
		if(!node) { return; }
    if(this.ownerCt && Ext.isFunction(this.ownerCt.getBottomToolbar)) {
      var bbar = this.ownerCt.getBottomToolbar();
      if(bbar) { Ext.fly(bbar.getEl()).scrollIntoView(node); }
    }
	},
	
	scrollRecordIntoView: function(Record) {
		if(!this.getStore()) { return; }
		
		//if(Record == Record.store.getLastRecord()) {
		//	return this.scrollBottomToolbarIntoView();
		//}
    
    var recNode = this.getNode(Record);
		
		//var node = this.getParentScrollNode(this.getEl().dom);
    var node = this.getParentScrollNode(recNode);
		if(!node) { return; }
		Ext.fly(recNode).scrollIntoView(node);

	},
  
  scrollNodeClass: null,

  getParentScrollNode: function(node) {
    if(!node) { return null; }
    if(this.scrollNodeClass) {
      var nodeEl = new Ext.Element(node);
      if(nodeEl.hasClass(this.scrollNodeClass)) {
        return node;
      }
    }
    if(node.style && (
      node.style.overflow == 'auto' || 
      node.style.overflow == 'scroll'
    )){ return node; }
      
    if(node.parentNode) {
      return this.getParentScrollNode(node.parentNode); 
    }
    return null;
  },

  // proof-of-concept: preliminary styles moved out of 045-AppDV.css to be generated
  // on-the-fly for each AppDB component. This is being done so that the override
  // style will match ONLY the correct AppDV, and not any nested AppDV modules
  dynStylesheetTpl: new Ext.XTemplate("<style type=\"text/css\">            \
    #{id}.ra-dsapi-deny-destroy .appdv-tt-generated.{id} .delete-record {   \
      display: none !important;                                             \
    }                                                                       \
  </style>").compile(),
  
  injectDynamicStylesheet: function() {
    this.dynStylesheetTpl.append(this.el,{ id: this.id });
  }

});
Ext.reg('appdv', Ext.ux.RapidApp.AppDV.DataView);

Ext.ux.RapidApp.AppDV.FieldPlugin = Ext.extend(Ext.util.Observable,{

  constructor: function(config){
    this.addEvents('valid','invalid');
    this.on('valid',this.onFieldValid,this);
    this.on('invalid',this.onFieldInvalid,this);
  },
    
  init: function(Field) {
    var Ctx = Field.AppDv_context;
    if(!Ctx) { return; }
    
    this.Field = Field; 
    this.Ctx = Ctx;
    
    var recNode = Ctx.appdv.getNode(Ctx.index);
    if(recNode) { 
      Ctx.recEl = new Ext.Element(recNode); 
    }

    this.relayEvents(Field,['valid','invalid']);
  },
    
  onFieldValid: function() {
    var El = this.Ctx.editEl;
    if(!El) { return; }
    
    if(El.hasClass('appdv-field-invalid')) {
      El.removeClass('appdv-field-invalid');
    }
    this.checkRecEl.defer(100,this);
  },
  onFieldInvalid: function(Field,msg) {
    var El = this.Ctx.editEl, recEl = this.Ctx.recEl;
    if(!El) { return; }

    if(!El.hasClass('appdv-field-invalid')) {
      El.addClass('appdv-field-invalid');
    }
    
    if(recEl && !recEl.hasClass('appdv-rec-invalids')) {
      recEl.addClass('appdv-rec-invalids');
    }
  },
  checkRecEl: function() {
    var recEl = this.Ctx.recEl;

    if(! recEl || ! recEl.hasClass('appdv-rec-invalids')) {
      return;
    }
    
    // If the record is marked invalid, but there are no longer
    // any field invalids, clear it
    if(! recEl.child('div.appdv-field-invalid')){
      if(recEl.hasClass('appdv-rec-invalids')) {
        recEl.removeClass('appdv-rec-invalids');
      }
    }
  }
   

});
Ext.preg('appdv-field-plugin',Ext.ux.RapidApp.AppDV.FieldPlugin);
