var Neumond = {
  version: '0.1.0',
  getIFrameDocument:function(ifr){
    if (ifr.contentDocument) return ifr.contentDocument;
    if (ifr.contentWindow && ifr.contentWindow.document) return ifr.contentWindow.document;
    return ifr.document;
  },
  tinyMCELoaded:false,
  onTinyMCELoad:(function(){
    this.tinyMCELoaded = true;
    if (document.loaded) this.nInitMCEFormInstances();
  }).bind(Neumond),
  onDomLoad:function(){
    Neumond.Curtain.initialize();
    this.nInitNativeFormInstances();
    if (!('tinyMCE' in window) || this.tinyMCELoaded) this.nInitMCEFormInstances();
  },
  nInitNativeFormInstances:function(){
    this.nNativeFormInstances.each(function(f){
      f.finalInit();
    });
  },
  nInitMCEFormInstances:function(){
    this.nMCEFormInstances.each(function(f){
      f.finalInit();
    });
  },
  nNativeFormInstances:[],
  nMCEFormInstances:[]
}

/**
*
*  Base64 encode / decode
*  http://www.webtoolkit.info/
*
**/
 
var Base64 = {
 
	// private property
	_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
 
	// public method for encoding
	encode : function (input) {
		var output = "";
		var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
		var i = 0;
 
		input = Base64._utf8_encode(input);
 
		while (i < input.length) {
 
			chr1 = input.charCodeAt(i++);
			chr2 = input.charCodeAt(i++);
			chr3 = input.charCodeAt(i++);
 
			enc1 = chr1 >> 2;
			enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
			enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
			enc4 = chr3 & 63;
 
			if (isNaN(chr2)) {
				enc3 = enc4 = 64;
			} else if (isNaN(chr3)) {
				enc4 = 64;
			}
 
			output = output +
			this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
			this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
 
		}
 
		return output;
	},
 
	// public method for decoding
	decode : function (input) {
		var output = "";
		var chr1, chr2, chr3;
		var enc1, enc2, enc3, enc4;
		var i = 0;
 
		input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
 
		while (i < input.length) {
 
			enc1 = this._keyStr.indexOf(input.charAt(i++));
			enc2 = this._keyStr.indexOf(input.charAt(i++));
			enc3 = this._keyStr.indexOf(input.charAt(i++));
			enc4 = this._keyStr.indexOf(input.charAt(i++));
 
			chr1 = (enc1 << 2) | (enc2 >> 4);
			chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
			chr3 = ((enc3 & 3) << 6) | enc4;
 
			output = output + String.fromCharCode(chr1);
 
			if (enc3 != 64) {
				output = output + String.fromCharCode(chr2);
			}
			if (enc4 != 64) {
				output = output + String.fromCharCode(chr3);
			}
 
		}
 
		output = Base64._utf8_decode(output);
 
		return output;
 
	},
 
	// private method for UTF-8 encoding
	_utf8_encode : function (string) {
		string = string.replace(/\r\n/g,"\n");
		var utftext = "";
 
		for (var n = 0; n < string.length; n++) {
 
			var c = string.charCodeAt(n);
 
			if (c < 128) {
				utftext += String.fromCharCode(c);
			}
			else if((c > 127) && (c < 2048)) {
				utftext += String.fromCharCode((c >> 6) | 192);
				utftext += String.fromCharCode((c & 63) | 128);
			}
			else {
				utftext += String.fromCharCode((c >> 12) | 224);
				utftext += String.fromCharCode(((c >> 6) & 63) | 128);
				utftext += String.fromCharCode((c & 63) | 128);
			}
 
		}
 
		return utftext;
	},
 
	// private method for UTF-8 decoding
	_utf8_decode : function (utftext) {
		var string = "";
		var i = 0;
		var c = c1 = c2 = 0;
 
		while ( i < utftext.length ) {
 
			c = utftext.charCodeAt(i);
 
			if (c < 128) {
				string += String.fromCharCode(c);
				i++;
			}
			else if((c > 191) && (c < 224)) {
				c2 = utftext.charCodeAt(i+1);
				string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
				i += 2;
			}
			else {
				c2 = utftext.charCodeAt(i+1);
				c3 = utftext.charCodeAt(i+2);
				string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
				i += 3;
			}
 
		}
 
		return string;
	}
 
}

Neumond.DragController = {
  activeDragObject:false,
  eMouseMove:function(e){
    if (false===this.activeDragObject){ return; }
    this.activeDragObject.eMouseMove(e);
  },
  eMouseUp:function(e){
    if (false===this.activeDragObject){ return; }
    this.activeDragObject.eMouseUp(e);
    this.activeDragObject = false;
    Event.stopObserving(document, 'mousemove');
    Event.stopObserving(document, 'mouseup');
  },
  trackObject:function(o,e){
    if (!this.activeDragObject && o.eMouseMove && o.eMouseUp){
      if(e.preventDefault){
        e.preventDefault();
      }
      this.activeDragObject = o;
      document.observe('mousemove', this.eMouseMove.bindAsEventListener(this));
      document.observe('mouseup', this.eMouseUp.bindAsEventListener(this));
    }
  }
}

Neumond.Observable = Class.create({
  initialize:function(eventList){//comma-delimited list of events
    this.observers = {};
    var x = eventList.split(',');
    
    for(var i=0;i<x.length;i++){
      this.observers[x[i]] = [];
    }
  },
  observe:function(evt, handler){
    if(evt in this.observers){
      this.observers[evt].push(handler);
    }
    return this;
  },
  stopObserving:function(evt, func){
    if(Object.isUndefined(evt)){
      this.observers = {};
    } else if(evt in this.observers){
      if (Object.isUndefined(func)) this.observers[evt].length = 0;
      else {
        var i=this.observers[evt].indexOf(func);
        if(i>=0) delete this.observers[evt][i];
      }
    }
    return this;
  },
  fireEvent:function(name, data){
    if(name in this.observers){
      data = data || {};
      data.evObj = this;
      for(var i=0;i<this.observers[name].length;i++){
        this.observers[name][i](data);
      }
      if (this.observers[name].length) return true;
    }
    return false;
  }
});

Neumond.Curtain = {
  initialize:function(){
    this.celm = new Element('div', {'class':'xcurtain','style':'display:none;'});
    this.loader = new Element('div', {'class':'xcurtain-loader','style':'display:none;'});
    this.loaderCapt = new Element('span');
    document.body.appendChild(this.celm);
    document.body.appendChild(this.loader);
    this.loader.appendChild(this.loaderCapt);
    this.loader.style.zIndex = (this.celm.style.zIndex ? this.celm.style.zIndex: 1000) + 1000;
    Event.observe(window, 'resize', this.wresize.bindAsEventListener(this));
    this.active = {curtain:false, elm:false, loader:false};
    this.wresize();
  },
  realign:function(){
    this.element.style.left = 
      Math.round((document.viewport.getWidth() - this.element.getWidth()) / 2) + 'px';
  },
  realignLoader:function(){
    this.loader.style.left = Math.round((document.viewport.getWidth()-this.loader.getWidth())/2)+'px';
    this.loader.style.top = Math.round((document.viewport.getHeight()-this.loader.getHeight())/2)+'px';
  },
  wresize:function(){
    if (this.active.elm){
      this.realign();
    }
    this.realignLoader();
  },
  check:function(){
    if (!(this.active.loader || this.active.elm)){
      this.celm.hide();
      this.active.curtain = false;
    }
  },
  showCurtain:function(){
    this.celm.show();
    this.active.curtain = true;
  },
  hideLoader:function(){
    this.active.loader = false;
    this.loader.hide();
    this.check();
  },
  hideElement:function(){
    this.active.elm = false;
    this.element.hide();
    this.check();
  },
  showLoader:function(caption){
    if (!this.active.curtain){this.showCurtain();}
    if (caption){this.loaderCapt.innerHTML = caption;}
    this.loader.show();
    this.active.loader = true;
  },
  show:function(element){
    if (!this.active.curtain){this.showCurtain();}
    //set these styles before using element there
    //position:absolute;top:[value];background:[value];width:[value];display:none(in style of element);
    this.element = $(element);
    this.element.style.zIndex = (this.celm.style.zIndex ? this.celm.style.zIndex: 1000) + 1;
    this.element.show();
    this.realign();
    this.active.elm = true;
  },
  hide:function(){
    if (this.active.loader){this.hideLoader();}
    if (this.active.elm){this.hideElement();}
    this.celm.hide();
    this.active.curtain = false;
  }
}

Neumond.Form = Class.create(Neumond.Observable, {
  initialize:function($super, elm, rootName, defaultValues, mode){
    $super('init,start,success,fail,complete,values');
    this.processing = true;
    this.mode = mode;
    this.form = $(elm);
    this.rootName = rootName;
    this.defaultValues = defaultValues || {};
    this.form.select('a.xform-attach-link').each(function(e){
      e.href = 'javascript:;';
    });
    
    this.additionalPostData = {};
    if (mode==2){
      var iframeName = this.form.target;
      this.iframe = new Element('iframe', {
        'name':iframeName,
        width:400,
        height:300,
        frameborder:1,
        hspace:0,
        vspace:0,
        src:'javascript:;',
        'class':'xform-target'
      });
      this.form.parentNode.appendChild(this.iframe);
    }
    if (mode==0 || mode==2){
      this.additionalDataNode = this.form.down('.xform-box.additional');
    }
    if (this.form.down('.xform-mceEditor')){
      Neumond.nMCEFormInstances.push(this);
    } else {
      Neumond.nNativeFormInstances.push(this);
    }
  },
  finalInit:function(){
    this.reset();
    this.processing = false;
    this.fireEvent('init');
  },
  reset:function(){
    this.values({});
  },
  values:function(data){
    this.fireEvent('values', {values:data});
    var v,n,foe,valueExist;
    var els=this.form.getElements();
    var r = new RegExp('^'+this.rootName+'\\[([^\\[\\]]*)\\](?:\\[\\])?$');
    for(var i=0;i<els.length;i++){
      if (!els[i].match('.xform-input')){continue;}
      n = r.exec(els[i].name);
      if (!n || !n[1]){continue;}
      n = n[1];
      //TODO: make array values possible to use
      if(n in data){
        v=data[n];
      }else if(n in this.defaultValues){
        v=this.defaultValues[n];
      }else{
        v='';
      }
      if(els[i].match('.xform-input-checkbox')){
        els[i].checked=Boolean(parseInt(v));
      }else if(els[i].match('select')){
        if (els[i].attributes.getNamedItem('multiple')){
          if (!v){v = [];}
          for(var j=0;j<els[i].options.length;j++){
            els[i].options[j].selected = !!(v.indexOf(els[i].options[j].value)>=0);
          }
        } else {
          if (null===v) v = '';
          for(var j=0;j<els[i].options.length;j++){
            if (els[i].options[j].value==v){
              els[i].selectedIndex=j;break;
            }
          }
        }
      }else if(els[i].match('.xform-input-file')){
        if (els[i].match('.xform-attach *')){
          var attachBox = els[i].up().down('.xform-attach-box');
          var attacha = attachBox.down('a.xform-attach-link');
          var attachd = attachBox.down('input.xform-attach-delete');
          if (attachd){attachd.checked = false;}
          if (v){
            attachBox.show();
            attacha.href = v;
          } else {
            attachBox.hide();
            attacha.href = 'javascript:;';
          }
        }
        els[i].value=''; //no value for file input
      }else if(els[i].match('.xform-input-password')){
        var le = els[i].up().down('.xform-password-lock');
        if (le){
          var lce = els[i].up().down('.xform-password-lock-c');
          if (v!==false){
            lce.show();
            le.checked = false;
            els[i].disable();
          } else {
            lce.hide();
            els[i].enable();
          }
        }
        els[i].value=''; //no value for password input
      }else if(els[i].match('.xform-mceEditor')){
        tinyMCE.editors[els[i].id].execCommand('mceSetContent', false, v);
      }else{
        els[i].value=v;
      }
    }
  },
  title:function(text){
    var e=this.form.down('.xform-title h3');
    if (e){e.innerHTML=text;}
  },
  clearAdvices:function(){
    this.form.select('.xform-advice').each(function(e){
      e.remove();
    });
    this.form.select('.xform-invalid').each(function(e){
      e.removeClassName('xform-invalid');
    });
  },
  validate:function(){
    this.clearAdvices();
    //TODO: make js validation
    return true;
  },
  showAdvice:function(fname, text){
    fname=this.rootName+'['+fname+']';
    var e=false;
    if (this.form[fname]){
      e=$(this.form[fname]);
    } else {
      fname+='[]';
      if (this.form[fname]){ e=$(this.form[fname]); }
    }
    if (e){
      e.up('.xform-box').addClassName('xform-invalid');
      var a=new Element('div', {'class':'xform-advice'}).update(text);
      e.up('.xform-box').appendChild(a);
    }
  },
  parseResponse:function(t){
    if (!t){this.fireEvent('fail',{errtype:0});return;}
    try {var res=eval('('+t+')');}
    catch (e){this.fireEvent('fail',{errtype:0});return;}
    if (!('error' in res)){this.fireEvent('fail',{errtype:0});return;}
    switch(res.error){
      case 0:
        this.fireEvent('success', res);
        return;
      case 1:
        if (!this.fireEvent('fail',{errtype:1,errmsg:res.error_msg})) alert(res.error_msg);
        break;
      case 2:
        this.fireEvent('fail',{errtype:2,validation:res.validation});
        for(var j in res.validation) this.showAdvice(j, res.validation[j]);
        break;
      default:
        this.fireEvent('fail',{errtype:0});
    }
    this.reloadCaptcha();
  },
  mergeAdditional:function(){
    this.additionalDataNode.childElements().each(function(e){e.remove();});
    var h;
    for (var i in this.additionalPostData){
      h = new Element('input', {
        'type':'hidden',
        'name':i,
        'value':this.additionalPostData[i]
      });
      this.additionalDataNode.appendChild(h);
    }
  },
  submit:function(){
    if (this.processing){return;}
    this.processing = true;
    this.fireEvent('start');
    if (this.validate()){
      switch (this.mode){
        case 1:
          this.ajaxSubmit();
          break;
        case 2:
          this.iframeSubmit();
          break;
        default:
          this.nativeSubmit();
      }
    }
  },
  nativeSubmit:function(){
    this.mergeAdditional();
    this.form.submit();
  },
  ajaxSubmit:function(){
    this.form.select('.xform-mceEditor').each(function(e){
      tinyMCE.editors[e.id].save();
    });
    var params = this.form.serialize({hash:true});
    Object.extend(params, this.additionalPostData);
    this.disable();
    new Ajax.Request(this.form.action, {
      method:this.form.method,
      parameters:params,
      onSuccess:function(t){
        this.parseResponse(t.responseText);
      }.bind(this),
      onFailure:function(){
        this.fireEvent('fail',{errtype:0});
        this.reloadCaptcha();
      }.bind(this),
      onComplete:function(){
        this.enable();
        this.processing = false;
        this.fireEvent('complete');
      }.bind(this)
    });
  },
  iframeSubmit:function(){
    this.mergeAdditional();
    this.ifrTimeout = this.iframeError.bind(this).delay(10.0);
    Event.observe(this.iframe, 'load', this.iframeDone.bind(this));
    this.form.submit();
    this.disable();
  },
  iframeComplete:function(){
    Event.stopObserving(this.iframe, 'load');
    this.enable();
    this.processing = false;
    this.fireEvent('complete');
  },
  iframeDone:function(){
    window.clearTimeout(this.ifrTimeout);
    this.parseResponse(Base64.decode(Neumond.getIFrameDocument(this.iframe).body.innerHTML));
    this.iframeComplete();
  },
  iframeError:function(){
    this.fireEvent('fail',{errtype:0});
    this.reloadCaptcha();
    this.iframeComplete();
  },
  focusAtFirst:function(){
    var els = this.form.getElements();
    if (els.length){
      els[0].focus();
    }
  },
  disable:function(){
    var els=this.form.getElements();
    for(var i=0;i<els.length;i++){
      if (els[i].disabled) els[i].xFormDisabled = true;
    }
    this.form.disable();
  },
  enable:function(){
    this.form.enable();
    var els=this.form.getElements();
    for(var i=0;i<els.length;i++){
      if ('xFormDisabled' in els[i]){
        els[i].disable();
        delete els[i].xFormDisabled;
      }
    }
  },
  reloadCaptcha:function(){
    var img = this.form.down('.xform-captcha-img');
    if (!img) return;
    img.src = img.src.split('?')[0] + '?time=' + (new Date).getTime();
  }
});

Neumond.Slider = Class.create(Neumond.Observable, {
  initialize:function($super, elm, config){
    $super('change,startChange,finishChange');
    this.elm = $(elm);
    this.createElements();
    if (!config) {config = {};};
    this.holdF = false;
    this.dragPt = 0;
    this.pos = 0;
    this.value = 0;
    this.minValue = 'minValue' in config ? config.minValue : 0;
    this.maxValue = 'maxValue' in config ? config.maxValue : 100;
    this.maxPos = this.ctnrElm.getWidth()-this.thumbElm.getWidth();
    this.setValue('value' in config ? config.value : 0);
    this.createEvents();
    this.enabled = true;
  },
  observe:function($super, evt, handler){
    $super(evt, handler);
    if(evt=='change'){
      this.fireEvent(evt, {value:this.value});
    }
    if(evt=='startChange' && this.holdF){
      this.fireEvent(evt, {value:this.value});
    }
  },
  createElements:function(){
    this.ctnrElm=new Element('div', {'class':'n-slider'});
    this.elm.appendChild(this.ctnrElm);
    this.thumbElm=new Element('div', {'class':'n-slider-thumb'});
    this.ctnrElm.appendChild(this.thumbElm);
    this.focusElm=new Element('a', {'class':'n-slider-focus','href':'javascript:;'});
    this.thumbElm.appendChild(this.focusElm);
  },
  createEvents:function(){
    this.focusElm.observe('mousedown', this.eMouseDown.bindAsEventListener(this));
  },
  eMouseDown:function(e){
    if (!this.enabled) {return;}
    this.dragPt = e.pointerX();
    this.holdF = true;
    Neumond.DragController.trackObject(this,e);
    this.fireEvent('startChange', {'value':this.value});
  },
  eMouseUp:function(e){
    this.holdF = false;
    this.fireEvent('finishChange', {'value':this.value});
  },
  eMouseMove:function(e){
    if (this.holdF && (e.pointerX()!=this.dragPt)){
      var oldPos = this.pos;
      this.trySetPos(this.pos + e.pointerX() - this.dragPt);
      this.dragPt = this.dragPt + this.pos - oldPos;
    }
  },
  setConstraints:function(min,max){
    this.minValue=min;
    this.maxValue=max;
    this.setValue(this.value);
  },
  setValue:function(x){
    var hf=this.holdF;
    this.holdF = false;
    this.doValueChange(parseInt(x));
    this.rePaintByValue();
    this.holdF = hf;
  },
  doValueChange:function(nv){
    if (nv<this.minValue){nv=this.minValue;}
    if (nv>this.maxValue){nv=this.maxValue;}
    if (nv!=this.value){
      this.fireEvent('change', {value:nv});
      this.value = nv;
      this.rePaintByValue();
      return true;
    }
    return false;
  },
  trySetPos:function(newPos){
    if (newPos<0){newPos=0;}
    if (newPos>this.maxPos){newPos=this.maxPos;}
    var v = Math.round(newPos / this.maxPos * (this.maxValue - this.minValue) + this.minValue);
    if (this.doValueChange(v)){
      this.rePaintByValue();
    }
  },
  rePaintByValue:function(){
    this.pos = Math.round((this.value - this.minValue) / (this.maxValue - this.minValue) * this.maxPos);
    this.thumbElm.style.left = this.pos + 'px';
  },
  enable:function(){
    this.enabled = true;
    this.elm.removeClassName('disabled');
  },
  disable:function(){
    this.enabled = false;
    this.elm.removeClassName('enabled');
  }
});

Neumond.Pager = Class.create(Neumond.Observable, {
  initialize:function($super, elm, config){
    $super('page');
    if (!config){config = {};}
    this.elm = $(elm);
    this.visibleCount = 'visibleCount' in config ? config.visibleCount : 10;
    this.currentPage = false;
    this.createElements();
    this.setPageCount('pageCount' in config ? config.pageCount : 200);
    this.setCurrentPage(1, true);
    this.enabled = true;
  },
  createElements:function(){
    this.ctnrElm = new Element('div', {'class':'n-paging'});
    this.elm.appendChild(this.ctnrElm);
    this.pagesElm = new Element('div', {'class':'n-paging-items'});
    this.ctnrElm.appendChild(this.pagesElm);
    this.listElm = new Element('ul');
    this.pagesElm.appendChild(this.listElm);
    this.itemElms = [];
    var ielm, aelm;
    for(var i=0;i<this.visibleCount;i++){
      ielm = new Element('li');
      aelm = new Element('a', {'href':'javascript:;'});
      ielm.appendChild(aelm);
      this.listElm.appendChild(ielm);
      this.itemElms.push(aelm);
      aelm.observe('click', this.selectPage.bindAsEventListener(this));
    }
    this.sliderElm = new Element('div', {'class':'n-paging-slider'});
    this.ctnrElm.appendChild(this.sliderElm);
    this.slider = new Neumond.Slider(this.sliderElm);
    this.slider.observe('change', function(e){
      if(this.enabled){
        this.scroll(e);
      }
    }.bind(this));
  },
  selectPage:function(evt){
    if (this.enabled){
      this.setCurrentPage(evt.element().nId);
    }
  },
  setPageCount:function(x){
    if (x<=0) x=1;
    if (x<this.currentPage) this.setCurrentPage(x);
    this.pageCount = x;
    if (x<=this.visibleCount){
      for(var i=0;i<this.visibleCount;i++){
        if(i<x){
          this.itemElms[i].up().show();
        }else{
          this.itemElms[i].up().hide();
        }
      }
      this.slider.setValue(0);
      this.sliderElm.hide();
    } else {
      for(var i=0;i<this.visibleCount;i++){
        this.itemElms[i].up().show();
      }
      this.sliderElm.show();
      this.slider.setConstraints(0, x-this.visibleCount);
    }
  },
  scroll:function(e){
    for(var i=0;i<this.visibleCount;i++){
      this.itemElms[i].innerHTML = (e.value+i+1);
      this.itemElms[i].nId = e.value+i+1;
      if (e.value+i+1==this.currentPage){
        this.itemElms[i].addClassName('selected');
      } else {
        this.itemElms[i].removeClassName('selected');
      }
    }
  },
  setCurrentPage:function(i,preventFiring){
    if (i<=0) i=1;
    if (i>this.pageCount) i=this.pageCount;
    if (i!=this.currentPage){
      this.currentPage = i;
      this.scroll({value:this.slider.value});
      if (!preventFiring){ this.fireEvent('page', {id:i}); }
    }
  },
  enable:function(){
    this.enabled = true;
    this.elm.removeClassName('disabled');
  },
  disable:function(){
    this.enabled = false;
    this.elm.removeClassName('enabled');
  }
});

Neumond.List = Class.create(Neumond.Observable, {
  initialize:function($super, config){
    $super('x'); //TODO: make there some events?..
    
    this.initMode = true;
    
    //row elements, stored values: nId{false|number}, nSelected{false|true}
    this.itemElms = [];
    
    this.mainElm = $(config.mainElm);
    this.elms = {};
    for(var i in config.elms){
      this.elms[i] = this.mainElm.down(config.elms[i]);
    }
    
    this.itemContainerCfg = config.itemContainerCfg;
    this.itemTemplate = new Template(config.itemTemplate);

    this.dataPKField = config.dataPKField;
    
    this.external = {};
    for (i=0;i<config.external.length;i++){
      var k = config.external[i];
      var ko,kn;
      if (!Object.isFunction(k)){
        ko = new Neumond.List.External[k](this); kn = k;
      } else {
        ko = new k(this); kn = ko.name; delete ko.name;
      }
      this.external[kn] = {};
      if (!kn in config.externalOpts) config.externalOpts[kn] = {};
      for (var j in ko){
        this.external[kn][j] = ko[j].bind(this, config.externalOpts[kn]);
      }
    }
    
    this.externalOpts = config.externalOpts;
    this.invokeAllExt('init', config);
    this.setLoaderMask(0);
    this.initMode = false;
    
    if ('initialData' in config) this.feedData(config.initialData);
  },
  invokeAllExt:function(fname, p){
    for (var e in this.external){
      if (fname in this.external[e]){
        this.external[e][fname](p);
      }
    }
  },
  setLoaderMask:function(v){
    if (v){this.elms.loadermask.show();}else{this.elms.loadermask.hide();}
  },
  updateItem:function(e,data){
    e.update(this.itemTemplate.evaluate(data));
    e.nId = (this.dataPKField in data) ? data[this.dataPKField] : false;
  },
  setItemVisibility:function(e, v){
    if (v){
      e.removeClassName(this.itemContainerCfg.invisibleClass);
    }else{
      e.addClassName(this.itemContainerCfg.invisibleClass);
      e.nId = false;
    }
  },
  isItemVisible:function(e){
    return !e.hasClassName(this.itemContainerCfg.invisibleClass);
  },
  setItemCount:function(x){
    var e;
    if(x>this.itemElms.length){
      while(this.itemElms.length<x){
        e = new Element(this.itemContainerCfg.tag, this.itemContainerCfg.options);
        this.itemElms.push(e);
        this.elms.root.appendChild(e);
        this.setItemVisibility(e, false);
        this.updateItem(e, {});
      }
    }else{
      while(this.itemElms.length>x){
        e = this.itemElms.pop();
        e.remove();
      }
    }
  },
  feedData:function(data){
    if (!('items' in data)) return;
    
    if ('itemCount' in data) this.setItemCount(data.itemCount);
    
    this.invokeAllExt('feedData', data);
    
    var j=0,e;
    for(var i=0;i<data.items.length;i++){
      if (j>=this.itemElms.length){break;}
      e = this.itemElms[j];
      this.updateItem(e, data.items[i]);
      this.setItemVisibility(e, true);
      this.invokeAllExt('item', e);
      
      j++;
    }
    while(j<this.itemElms.length){
      e = this.itemElms[j];
      this.setItemVisibility(e, false);
      j++;
    }
  },
  callExt:function(n, p){
    var fn=n.split(':');
    if (fn.length<2) fn.push('main');
    if (fn[0] in this.external && fn[1] in this.external[fn[0]]){
      return this.external[fn[0]][fn[1]](p);
    }
    return false;
  },
  checkResponse:function(data){
    if (typeof data == 'object' && data !== null && 'error' in data){
      if (data.error != 0){
        this.showError(data.error_msg);
        return false;
      }
      return true;
    }
    this.showConnectError();
    return false;
  },
  showError:function(e){
    alert(e);
  },
  showConnectError:function(){
    this.showError('Connection error');
  }
});

Neumond.List.External = {}

//TODO: Neumond.List.External.limits

Neumond.List.External.update = function(lstObj){
  this.main = function(opts){
    this.setLoaderMask(1);
    var p={};
    p.limit = this.itemElms.length;
    if ('pages' in this.external) p.page = this.pager.currentPage;
    //add there parameters from other externals (limit, page, etc.)
    new Ajax.Request(opts.url, {
      parameters:p,
      method:'post',
      onSuccess:function(t){
        var data = t.responseJSON;
        if (this.checkResponse(data)){
          this.feedData(data);
        }
      }.bind(this),
      onFailure:function(t){
        this.showConnectError();
      }.bind(this),
      onComplete:function(t){
        this.setLoaderMask(0);
      }.bind(this)
    });
  }
}

Neumond.List.External.globalCount = function(lstObj){
  this.init = function(opts, config){
    this.callExt('globalCount:set', 0);
  }
  this.feedData = function(opts, data){
    if ('globalCount' in data){
      this.callExt('globalCount:set', data.globalCount);
    }
  }
  this.set = function(opts, c){
    if (typeof c != 'number') c=parseInt(c);
    this.globalCount=c;
    this.elms.globalCount.innerHTML = c;
    if (!this.initMode && 'pages' in this.external) this.callExt('pages:globalCountUpdate');
  }
}

//requires update, globalCount
Neumond.List.External.pages = function(lstObj){
  this.init = function(opts, config){
    this.pager = new Neumond.Pager(this.elms.pagerKeeper, {
      pageCount:1,
      visibleCount:opts.visibleCount
    });
    this.pager.observe('page', function(opts, evt){
      this.callExt('update');
      /*
      if ('updateUrl' in opts){ //TODO: make there more usable condition
        var a=location.href.split('#');
        a[1] = evt.id;
        location.href = a.join('#');
      }
      */
    }.bind(this, opts));
  }
  this.feedData = function(opts, data){
    if ('page' in data){
      this.pager.setCurrentPage(data.page, true);
    }
  }
  this.globalCountUpdate = function(opts){
    var pc = 0;
    if (this.itemElms.length>0){
      pc = Math.ceil(this.globalCount / this.itemElms.length);
    }
    this.pager.setPageCount(pc);
  }
}

Neumond.List.External.select = function(lstObj){
  selectItemC = function(e){
    e.addClassName(this.itemContainerCfg.selectedClass);
  }.bind(lstObj);
  deselectItemC = function(e){
    e.removeClassName(this.itemContainerCfg.selectedClass);
  }.bind(lstObj);
  selectItem = function(e){
    selectItemC(e);
    if (this.selectionInverse){
      if (e.nId in this.selectionIds) delete this.selectionIds[e.nId];
    } else {
      this.selectionIds[e.nId] = true;
    }
  }.bind(lstObj);
  deselectItem = function(e){
    deselectItemC(e);
    if (this.selectionInverse){
      this.selectionIds[e.nId] = true;
    } else {
      if (e.nId in this.selectionIds) delete this.selectionIds[e.nId];
    }
  }.bind(lstObj);
  isItemSelected = function(id){
    if (this.selectionInverse){
      return !(id in this.selectionIds);
    } else {
      return (id in this.selectionIds);
    }
  }.bind(lstObj);
  this.init = function(opts, config){
    this.selectionInverse = false;
    this.selectionIds = {};
    this.callExt('select:updateSelectionCounter');
  }
  this.item = function(opts, e){
    if ((e.nId in this.selectionIds) + this.selectionInverse == 1){
      selectItemC(e);
    } else {
      deselectItemC(e);
    }
  }
  this.itemClick = function(opts, a){
    var e=a.up(this.itemContainerCfg.selector);
    if (!this.isItemVisible(e)) return;
    if (!isItemSelected(e.nId)){
      selectItem(e);
    } else {
      deselectItem(e);
    }
    this.callExt('select:updateSelectionCounter');
  }
  this.selectAll = function(opts){
    this.selectionInverse = true;
    this.selectionIds = {};
    this.callExt('select:selectVisible');
    this.callExt('select:updateSelectionCounter');
  }
  this.deselectAll = function(opts){
    this.selectionInverse = false;
    this.selectionIds = {};
    this.callExt('select:deselectVisible');
    this.callExt('select:updateSelectionCounter');
  }
  this.selectVisible = function(opts){
    for (var i=0;i<this.itemElms.length;i++){
      if (this.isItemVisible(this.itemElms[i])){
        selectItem(this.itemElms[i]);
      }
    }
    this.callExt('select:updateSelectionCounter');
  }
  this.deselectVisible = function(opts){
    for (var i=0;i<this.itemElms.length;i++){
      if (this.isItemVisible(this.itemElms[i])){
        deselectItem(this.itemElms[i]);
      }
    }
    this.callExt('select:updateSelectionCounter');
  }
  this.getSelectedCount = function(opts){
    var x=0;
    for (var i in this.selectionIds){x++;}
    if (this.selectionInverse){x = this.globalCount - x;}
    return x;
  }
  this.quietDeselectById = function(opts, id){
    if (id in this.selectionIds) delete this.selectionIds[id];
    this.callExt('select:updateSelectionCounter');
  }
  this.updateSelectionCounter = function(opts){
    this.elms.selectedCount.innerHTML = this.callExt('select:getSelectedCount');
  }
}

Neumond.List.External.remove = function(lstObj){
  rmRequest = function(opts, ids, exclude, cnfrm, cb){
    c = ids.length;
    if (exclude) { c = this.globalCount - c; }
    if (c<=0) return;
    if (cnfrm && !confirm('Do you really want to remove %count% items?'.replace('%count%', c))) return;
    this.setLoaderMask(1);
    exclude = exclude ? 1 : 0;
    var p={
      remove: ids.join(','),
      exclude: exclude
    };
    p['update[limit]'] = this.itemElms.length;
    if ('pages' in this.external) p['update[page]'] = this.pager.currentPage;
    //add there parameters from other externals (limit, page, etc.)
    new Ajax.Request(opts.url, {
      parameters:p,
      method:'post',
      onSuccess:function(t){
        var data = t.responseJSON;
        if (this.checkResponse(data)){
          if (cb) cb();
          this.feedData(data.update);
        }
      }.bind(this),
      onFailure:function(t){
        this.showConnectError();
      }.bind(this),
      onComplete:function(t){
        this.setLoaderMask(0);
      }.bind(this)
    });
  }.bind(lstObj);
  this.itemClick = function(opts, a){
    var e=a.up(this.itemContainerCfg.selector);
    if (!this.isItemVisible(e)) return;
    rmRequest(opts, [e.nId], false, false, function(x){
      this.callExt('select:quietDeselectById', x);
    }.bind(this, e.nId));
  }
  this.selected = function(opts){
    if (!('select' in this.external)) return;
    var ixs = [];
    for (var i in this.selectionIds){
      ixs.push(i);
    }
    rmRequest(opts, ixs, this.selectionInverse, true, function(){
      this.callExt('select:deselectAll');
    }.bind(this));
  }
}

Neumond.List.External.edit = function(lstObj){
  updateFormAdditionalData = function(opts){
    var d = {};
    d['update[limit]'] = this.itemElms.length;
    if ('pages' in this.external) d['update[page]'] = this.pager.currentPage;
    opts.form.additionalPostData = d;
  }.bind(lstObj);
  
  this.init = function(opts, config){
    opts.form = window[opts.form];
    if (!'canEdit' in opts) opts['canEdit'] = 0;
    if (!'canAdd' in opts) opts['canAdd'] = 0;
    opts.form.observe('start', function(){
      if ('editCurtainBox' in this.elms){
        Neumond.Curtain.showLoader('Loading...'); //TODO:
      }
    }.bind(this));
    opts.form.observe('success', function(res){
      this.feedData(res.update);
      if ('editCurtainBox' in this.elms){
        Neumond.Curtain.hide();
      }
    }.bind(this));
    opts.form.observe('fail', function(res){
      if (res.errtype==0) this.showConnectError();
      if (res.errtype==1) this.showError(res.errmsg);
    }.bind(this));
    opts.form.observe('complete', function(){
      if ('editCurtainBox' in this.elms){
        Neumond.Curtain.hideLoader();
      }
    }.bind(this));
  }
  
  this.newItem = function(opts){
    if (!opts.canAdd) return;
    this.editItemId = false;
    updateFormAdditionalData(opts);
    opts.form.clearAdvices();
    opts.form.values({});
    opts.form.title('Add new item'); //TODO:
    if ('editCurtainBox' in this.elms) Neumond.Curtain.show(this.elms.editCurtainBox);
    opts.form.focusAtFirst();
  }
  
  this.editItem = function(opts, id){
    if (!opts.canEdit) return;
    this.editItemId = id;
    this.setLoaderMask(1);
    new Ajax.Request(opts.getUrl.replace('#', id), {
      method:'get',
      onSuccess:function(opts, t){
        var d = t.responseJSON;
        if (this.checkResponse(d)){
          updateFormAdditionalData(opts);
          opts.form.clearAdvices();
          opts.form.values(d.data);
          opts.form.title('Edit item #%id%'.replace('%id%', this.editItemId)); //TODO:
          if ('editCurtainBox' in this.elms) Neumond.Curtain.show(this.elms.editCurtainBox);
          opts.form.focusAtFirst();
        }
      }.bind(this, opts),
      onFailure:function(){
        this.showConnectError();
      }.bind(this),
      onComplete:function(){
        this.setLoaderMask(0);
      }.bind(this)
    });
  }
  
  this.editItemClick = function(opts, a){
    var e=a.up(this.itemContainerCfg.selector);
    if (!this.isItemVisible(e)) return;
    this.callExt('edit:editItem', e.nId);
  }
}

document.observe('dom:loaded', Neumond.onDomLoad.bind(Neumond));
