// class SuggestController
// Author: Jan Elias
// Version: viz. SuggestController.version
// Last modified: 3.3.2008
// Implementation details:
//   ZACHYTANI SYSTEMOVYCH UDALOSTI
//   specialni implementace zachytavani systemovych udalosti, protoze
//   handler nesmi obsahovat identifikator this, proto je to delano pomoci 
//   statickych metod (controller factory) a eval (pri inicializaci)
//   SKRYTI NASEPTAVACE
//   specialni implementace, protoze prohlizece nepodporuji event.toElement
//   v udalosti onblur (krome IE > 6)
//   ZJISTENI EDITOVANEHO SLOVA
//   specialni implementace zjisteni editovaneho slova, protoze v zadnem prohlizeci
//   nelze zjistit pozici kurzoru v textovem poli
// Changelog:
// v7
//  - opravena chyba: nefunguje skryti naseptavace pri zmene 
// v6
//  - prepsano na pouziti DIV misto SELECT
//  - pridana inicializace systemovych udalosti v initialize()
//  - pridana udalost onvisibilitychange
// v5
//  - pridana moznost pouzit pro vyber klavesu enter (pouze IE)
//  - pridana vlastnost isVisible, ktera rika, zda je naseptavac zobrazen
// v4
//  - přidána možnost výběru jména myší (vyžaduje > IE 6.0) 
// v3
//  - optimalizace
// v2
//  - trida vyzaduje v konstruktoru objekt typu <input type=hidden ...>
//    kvuli ulozeni identifikatoru vybranych emailu

function SuggestItem(id, name) {
  this.id = id;
  this.name = name;
}

function SuggestController(oSelect, oText, oHidden, arrNames, autoHide, autoSize) {
  // otestuju povinna pole
  if (!oSelect || !oText || !oHidden)
    throw new Error("Neni definovan seznam emailu, pole, do ktereho se ma naseptavat, nebo skryte pole, ktere bude obsahovat seznam identifikatoru.");
  
  // priradim identifikator (musi byt v konstruktoru)
  this._id = SuggestController._newControllerId(this);
  
  // nastavim vsechny promenne (nepovinne na implicitni hodnoty)
  this.isVisible = this.oSelect != "hidden";
  this.debugWrite("select_visibility", this.isVisible);
  
  this.initText(oText, oHidden, arrNames);
  this.initSelect(oSelect, autoSize);
  this.initSelectHiding(autoHide);
  this._emptySearch();
}

SuggestController.version = 7;

/************ Podpora pro debugovani *************/

SuggestController.prototype.debug = false;
SuggestController.prototype.debugWrite = function (what, desc) {
  if (!this.debug) return;
  var o = document.getElementById(what);
  if (o != null) {
    if (o.innerText) // IE
      o.innerText = desc; 
    else if (o.innerHTML != null) // mozilla
      o.innerHTML = desc; 
  }
}
SuggestController.prototype.debugAppend = function (what, desc) {
  if (!this.debug) return;
  var o = document.getElementById(what);
  if (o != null) {
    if (o.innerText) // IE
      o.innerText += desc; 
    else if (o.innerHTML != null) // mozilla
      o.innerHTML += desc; 
  }
}

/************ Controller factory ************/
/*
 * Kvuli potrebe reagovat na systemove udalosti a kvuli tomu, ze pri registraci
 * systemove udalosti je potreba volat instanci objektu jmenem (ktere instance 
 * nezna, nelze ani pouzit identifikator this), je potreba zavest mechanismus, ktery
 * vrati spravnou instanci, ktera bude pak pouzita pri volani metody ze systemove 
 * udalosti.
 * Nevim, jestli factory je nejvystiznejsi slovo :)
 */

// staticke pole
SuggestController._instances = new Array();

SuggestController._newControllerId = function(oController) {
  SuggestController._instances[SuggestController._instances.length] = oController;
  return SuggestController._instances.length-1;
}

SuggestController._getControllerById = function(id) {
  if (SuggestController._instances == null || id > SuggestController._instances.length-1) 
    return null;
  
  return SuggestController._instances[id];
}

// metoda instance
SuggestController.prototype._getCallingObjectScript = function() {
  return "SuggestController._getControllerById("+this._id+")";
}

/************ Podpora registrace udalosti **********/

SuggestController.prototype.attachEvent = function (eventName, eventHandler) {
  // zatim mam jen jednu udalost
  switch(eventName) {
    case "onvisibilitychange":
      this._arrOnVisibilityChangeHandlers[this._arrOnVisibilityChangeHandlers.length] = eventHandler;
      break;
  }
}

/********** Podpora zobrazeni/skryti naseptavace ***********/

SuggestController.prototype.autoHide = false;
SuggestController.prototype.offsetLeft = 20;
SuggestController.prototype.offsetTop = 20;

SuggestController.prototype.initSelectHiding = function(autoHide) {
  if (autoHide)
    this.autoHide = true;

  this.oSelect.style.position = "absolute";
  this.oSelect.style.zIndex = 10;

  // skryju/zobrazim naseptavac (podle nastaveni)
  this.hideSelect();

  // tady je zachyceno ziskani/ztrata focusu pro zobrazeni/skryti naseptavace
  eval('var oText_onfocus = function(e){ SuggestController._getControllerById('+this._id+')._onfocus(e||window.event); };');
  
  if (this.oText.addEventListener)
    this.oText.addEventListener('focus', oText_onfocus, false);
  else if (this.oText.attachEvent)
    this.oText.attachEvent('onfocus', oText_onfocus);

  if (""+this.oText.ondeactivate != "undefined") {
    // tak se jedna o IE >= 5.5
    eval('var window_checkToElement = function() { SuggestController._getControllerById('+this._id+')._checkToElement(window.event, window.event.toElement); }'); //toElement >IE 6.0
    this.oText.attachEvent('ondeactivate', window_checkToElement); // - kvuli event.toElement
  } else {
    // ostatni prohlizece
    // testovano pouze pro FF 2.0
    eval('var oText_setCheckingToElement = function(e) { SuggestController._getControllerById('+this._id+')._setCheckingToElementFF(e||window.event); }'); 
    eval('var window_checkToElement = function(e) { SuggestController._getControllerById('+this._id+')._checkToElementFF(e||window.event); }');
    if (this.oText.addEventListener)
      this.oText.addEventListener('blur', oText_setCheckingToElement, false);
    else if (this.oText.attachEvent)
      this.oText.attachEvent('onblur', oText_setCheckingToElement); // IE < 5.5
  
    if (this.oSelect.addEventListener)
      this.oSelect.addEventListener('blur', oText_setCheckingToElement, false);
    else if (this.oSelect.attachEvent)
      this.oSelect.attachEvent('onblur', oText_setCheckingToElement); // IE < 5.5 

    /**** TOTO JE HACK, protoze potrebuju zjistit kam mizi focus ******/
    if (window.addEventListener)
      window.addEventListener('click', window_checkToElement, false);
    else if (window.attachEvent)
      window.attachEvent('onclick', window_checkToElement);
  
    if (window.addEventListener)
      window.addEventListener('keyup', window_checkToElement, false);
    else if (window.attachEvent)
      window.attachEvent('onkeyup', window_checkToElement);
  }
}

SuggestController.prototype._onfocus = function (e) {
  // pri ziskani focus zobraz select
  if (!this.autoHide) {
    this.showSelect();
    //this.oText.select();
  }
}

SuggestController.prototype._checkToElement = function (e, toElement) {
  var isBlur = true;
  // zjisti, kam se prenesl focus
  this.debugWrite("click_target", "?");  
  var obj = toElement;
  while (obj != null && obj != window && obj != document) {
    this.debugAppend("click_target", "->"+ obj.tagName);
    if (obj.tagName == "INPUT")
      this.debugAppend("click_target", "("+obj.type+")");
    if (obj == document.getElementById("selItems")) {
      // focus se prenesl na naseptavac nebo jeho deti
      isBlur = false;
      this.debugAppend("click_target", "(FOUND)");
    }
    obj = obj.parentNode;
  }
  this.debugWrite("blur", isBlur);

  // pri ztrate focusu skryj select
  if (isBlur)
    this.hideSelect();
}

SuggestController.prototype._setCheckingToElementFF = function (e) {
  /*
   * Mohlo by byt pouzito e.toElement, ale to funguje jen v IE > 6,
   * takze je potreba to implementovat nejak pro ostatni prohlizece.
   * Je implementovatno tak, ze se porad testuje nove kliknuti/zmacknuti klavesy (window.onclick,
   * window.onkeyup) po ztrate focusu na textovem poli (oText.onblur). Jestlize kliknuti
   * nebo zmacknuti klavesy (Tab) zpusobilo prechod na naseptavac, tak se blur nevyvola,
   * jinak ano.
   */
  // nastav, ze je pozadovano test na objekt, kteremu bude predan focus
  this.checkToElement = true;
}

SuggestController.prototype._checkToElementFF = function (e) {
  e = e||window.event;
  // jestlize kliknuti/zmacknuti klavesy predchazelo blur
  if (this.checkToElement) {
    this._checkToElement(e, e.target);
    
    // vynuluj priznak, protoze ztrata focusu jiz byla vyhodnocena
    this.checkToElement = false;
  } else
    this.debugWrite("blur", "");
}

SuggestController.prototype.showSelect = function () { 
  if (!this.isVisible) {
    this.oSelect.style.visibility="visible"; 
    this.oSelect.style.left = this.oText.offsetLeft + document.body.scrollLeft + this.offsetLeft +"px";
    this.oSelect.style.top = this.oText.offsetTop + document.body.scrollTop + this.offsetTop +"px";
    this.isVisible = true;
    this.debugWrite("select_visibility", this.isVisible);
    this.onvisibilitychange();
  }
}

SuggestController.prototype.hideSelect = function () { 
  if (this.isVisible) {
    this.oSelect.style.visibility="hidden"; 
    this.isVisible = false;
    this.debugWrite("select_visibility", this.isVisible);
    this.onvisibilitychange();
  }
}

SuggestController.prototype._arrOnVisibilityChangeHandlers = new Array();

SuggestController.prototype.onvisibilitychange = function(e) {
  for (var i=0; i<this._arrOnVisibilityChangeHandlers.length; i++)
    this._arrOnVisibilityChangeHandlers[i](e);
}

/************** Podpora vytvoreni naseptavace **************/

// Staticke konstanty 
SuggestController.SELECTED_CSS_CLASS_NAME = "item_selected";
SuggestController.ITEM_CSS_CLASS_NAME = "item";

// maximalni velikost
SuggestController.prototype.autoSize = false;
SuggestController.prototype.MAX_SELECT_SIZE = 5;

SuggestController.prototype.initSelect = function(oSelect, autoSize) {
  this.oSelect = oSelect;
  
  if (autoSize)
    this.autoSize = true;
}

// Podpora pro omezeni velikosti naseptavace 
SuggestController.prototype._setSelectSize = function(iSize) {
  if (!this.autoSize) return;
  
  var size = this.MAX_SELECT_SIZE;
  if (iSize>this.MAX_SELECT_SIZE)
    size = this.MAX_SELECT_SIZE;
  else if (iSize<1)
    size = 1;
  else 
    size = iSize;
  this.oSelect.style.height = size+"em";
  return size;
}

SuggestController.prototype._createOption = function(id, name, selected) {
  var cls = (selected ? SuggestController.SELECTED_CSS_CLASS_NAME : SuggestController.ITEM_CSS_CLASS_NAME);
  var callingObjectScript = this._getCallingObjectScript();
  var onclickScript = callingObjectScript +"._onclick(event,"+ id +");";
  var onmouseoverScript = callingObjectScript +"._onselect(event,"+ id +");";
  return "<div class='"+ cls +"' onclick='"+ onclickScript +"' onmouseover='"+ onmouseoverScript +"'>"+ name +"</div>";
}

SuggestController.prototype._onclick = function (e, newIndex) {
  // nejdrive je potreba oznacit spravny radek, abychom na to mohli spustit _completeWord
  this._selectIndex(newIndex);
  
  var arrWords = this.oText.value.split(";");
  this._completeWord(arrWords, arrWords.length-1);
  this.oText.value += ";";
  this._resetChange(this.oText.value);
  this.oText.focus(); 
  //this.oText.select();
}

SuggestController.prototype._onselect = function (e, newIndex) {
  this._selectIndex(newIndex);
}

SuggestController.prototype._getSelectedIndex = function() {
  var selectedIndex = -1;
  for (var i=0; i<this.oSelect.childNodes.length; i++)
    if (this.oSelect.childNodes[i].className == SuggestController.SELECTED_CSS_CLASS_NAME)
      selectedIndex = i;
   return selectedIndex;
}

SuggestController.prototype._selectIndex = function (newIndex) {
  // projdu vsechny divy v "selectu" (vlastne se take jedna o div :)
  for (var i=0; i<this.oSelect.childNodes.length; i++)
    if (this.oSelect.childNodes[i].className == SuggestController.SELECTED_CSS_CLASS_NAME && i != newIndex)
      this.oSelect.childNodes[i].className = SuggestController.ITEM_CSS_CLASS_NAME;
    else if (this.oSelect.childNodes[i].className == SuggestController.ITEM_CSS_CLASS_NAME && i == newIndex)
      this.oSelect.childNodes[i].className = SuggestController.SELECTED_CSS_CLASS_NAME;
}

SuggestController.prototype._emptySearch = function () {
  if (this.autoHide) {
    this.hideSelect();
  } else {
    // a provedu prvni naplneni selectu ve formulari
    var divs = new Array();
    for(var i=0;i<this.arrNames.length;i++) {
      // nezapomenout na vyber prvniho radku i==0
      divs[divs.length] = this._createOption(divs.length, this.arrNames[i].name, i==0);
    }
    this.oSelect.innerHTML = divs.join('');

    // nezapomenout na kontrolu velikosti selectu
    this._setSelectSize(this.oSelect.childNodes.length);
  }
}

/**** Podpora pro zjisteni pozice kurzoru a pro zjisteni editovaneho slova ****/

/*
 * Bohuzel nefunguje uplne presne, hlavne pokud pole obsahuje nekolik stejnych pismen za sebou
 * problem delaji hlavne stredniky.
 */ 

SuggestController.prototype._findChange = function() {
  var METHOD_PREFIX_FROM_END = 1;
  var METHOD_PREFIX_FROM_BEGINNING = 2;
  var METHOD_SUFFIX_FROM_BEGINNING = 3;
  var FIND_METHOD = 2;
  
  var thisPhrase = this.oText.value;
  var lastPhrase = this._lastPhrase;
  this._lastPhrase = thisPhrase;

  var shorter = thisPhrase.length < lastPhrase.length ? thisPhrase.length : lastPhrase.length;
  if (FIND_METHOD == METHOD_PREFIX_FROM_END) {
    var found = 0;
    for (var len=shorter; len>=0; len--)
      if (thisPhrase.substr(0, len) == lastPhrase.substr(0, len)) {
        found = len;
        break;
      }
  } else if (FIND_METHOD == METHOD_PREFIX_FROM_BEGINNING) {
    var found = shorter;
    for (var len=0; len<shorter; len++)
      if (thisPhrase.substr(0, len) != lastPhrase.substr(0, len)) {
        found = len-1;
        break;
      }
  } else if (FIND_METHOD == METHOD_SUFFIX_FROM_BEGINNING) {
    var found = shorter;
    for (var len=0; len<shorter; len++)
      if (thisPhrase.substr(thisPhrase.length-len, len) != lastPhrase.substr(lastPhrase.length-len, len)) {
        found = shorter-len;
        break;
      }
  }
  this.debugWrite("cursor", found);
  return found;
}

SuggestController.prototype._resetChange = function(newValue) {
  this._lastPhrase = newValue;
}

SuggestController.prototype._findIndex = function (arrWords, curPosition) {
  var len = 0;
  var iWord = 0;
  for (var i=0; i<arrWords.length; i++) {
    len += arrWords[i].length+1; // jednicka je tu za strednik
    if (curPosition <= len-1) { // jednicka je tu kvuli prepoctu z delky na index
      iWord = i;
      break;
    }
  }
  this.debugWrite("word", iWord);
  return iWord;
}

SuggestController.prototype._saveValues = function(arrWords) {
  var arrValues = new Array();
  for (var i=0; i<arrWords.length; i++) {
    var word = arrWords[i].replace(/^(\s*)/i, '');
    word = word.replace(/(\s*)$/i, '');
    for (var j=0; j<this.arrNames.length; j++)
      if (word == this.arrNames[j].name)
        arrValues[arrValues.length] = this.arrNames[j].id;
  }
  this.oHidden.value = arrValues.join(";");
}

/************ Vlastni implementace vyhledavani podle zadaneho pismene *************/

SuggestController.prototype.arrNames = new Array();

SuggestController.prototype.initText = function(oText, oHidden, arrNames) {
  this.oText = oText;
  this.oHidden = oHidden;

  if (arrNames != null)
    this.arrNames = arrNames;
    
  // inicializuju textove pole
  this._resetChange(this.oText.value);

  // tady je zachyceno vlastni psani jmena
  eval('var oText_onkey = function(e) { SuggestController._getControllerById('+this._id+')._onkey(e||window.event, e.keyCode); }');

  if (this.oText.addEventListener)
    this.oText.addEventListener('keyup', oText_onkey, false);
  else if (this.oText.attachEvent)
    this.oText.attachEvent('onkeyup', oText_onkey);
}

SuggestController.prototype._onkey = function (e, keyCode) {
  this.debugWrite("key", keyCode);

  if (keyCode == null) return;

  // nejdrive osetrim klavesy, ktere nemeni pole
  switch(keyCode) {
    case 13: /********* enter **********/
      if (this.isVisible)
        // pri vlozeni enteru doplnim slova stejne jako pri stisku stredniku
        this.oText.value += ";";
      break;
    case 27:
      this.hideSelect();
      return;
    /********** sipky doleva a doprava *********/
    case 37:
    case 39:
    /********** stranka vpred a vzad *********/
    case 33:
    case 34:
    /********** home a end *********/
    case 36:
    case 35:
      return;
    case 38: /********* sipka nahoru **********/
      if (!this.isVisible) {
        this.showSelect();
        return;
      }
      var selIndex = this._getSelectedIndex();
      if (selIndex == -1)
        this._emptySearch();
      else if (selIndex > 0) 
        this._selectIndex(selIndex-1);
      return;
    case 40: /********* sipka dolu **********/
      if (!this.isVisible) {
        this.showSelect();
        return;
      }
      var selIndex = this._getSelectedIndex();
      if (selIndex == -1)
        this._emptySearch();
      else if (selIndex < this.oSelect.childNodes.length-1)
        this._selectIndex(selIndex+1);
      return;
  }
  // zkusim najit index slova, ktere se pozaduje doplnit
  var curPosition = this._findChange();
  var arrWords = this.oText.value.split(";");
  var iWord = this._findIndex(arrWords, curPosition);

  switch(keyCode) {
    case 13: /********* enter **********/
    case 186: /********* strednik **********/ // IE !!!!!!
    case 59: /********* strednik **********/ // Mozilla !!!!!!
//    case 188: /********* carka **********/
      // pri vlozeni stredniku nebo carky doplnim slova podle selectu
      this._completeWord(arrWords, iWord);
      break;
    /*********** ostatni klavesy *************/
    default:
      if (iWord < arrWords.length-1) {
        // zmena na jinem nez poslednim slove -> proste slovo smaz
        // mohl bych slovo vyhodnotit, ale v tuto chvili nejsem schopen rozeznat
        // spravne, za kterym slovem byl zmacknuty strednik, a to konkretne 
        // v pripade, kdy v poli existuje vic stredniku za sebou
        var newArrWords = new Array();
        for (var i=0; i<arrWords.length; i++)
          if (i != iWord)
            newArrWords[newArrWords.length] = arrWords[i];
        arrWords = newArrWords;
        
        // a znovu vlozim do pole
        this.oText.value = arrWords.join(";");
        this._resetChange(this.oText.value);
        
        this._saveValues(arrWords);
        break;
      }
      var str = arrWords[iWord];
      // jestlize je textove pole prazdne, tak vytvorim znovu kompletni select
      if(str == '') {this._emptySearch(); break;}

      // odstran bila mista ze zacatku vyhledavaneho retezce
      str = str.replace(/^\s*/, '');      
      // inicializuju regexp
      pattern1 = new RegExp("^"+str,"i");
      this.debugWrite("regexp", "/^"+ str +"/i"); // debug
    
      // a projdu vsechny hodnoty pomocneho pole
      // a jestlize hodnota zacina stejne jako text
      // tak hodnotu pridam do selectu ve formulari
      var pocet = 0;
      var divs = new Array();
      for(var i=0;i<this.arrNames.length;i++) {
        var name = this.arrNames[i].name;
        if(pattern1.test(name))
          divs[divs.length] = this._createOption(divs.length, name, divs == "")
      }
      this.oSelect.innerHTML = divs.join('');
    
      // pri napsani jakehokoliv znaku zacnu zobrazovat naseptavac
      this.showSelect();
      
      this._setSelectSize(this.oSelect.childNodes.length);
      
      this._saveValues(arrWords);
      break;
  }
  
  if (keyCode == 13)
    return false;
  return true;
}

SuggestController.prototype._completeWord = function(arrWords, iWord) {
  // zkontroluju text mezi 
  // oddelovaci a pripadne doplnim texty podle selectu,
  // jestlize neni nic vybrano, je zbytecne neco delat
  var selectedIndex = this._getSelectedIndex();
  if (selectedIndex < 0) return;

  // z textu odstranim pocatecni bila mista
  // koncova nemusim odstranovat, protoze muzu chtit vyhledavat 
  // i podle mezery
  var strAkt = arrWords[iWord].replace(/^(\s*)/,'');
  if (strAkt == "") {
    arrWords[iWord] = this.oSelect.childNodes[selectedIndex].innerHTML;//this.arrNames[selectedIndex].name;
  } else {
    // pro kontrolu budu porovnavat regularnim vyrazem
    // bez bilych mist na zacatku a na konci a case insensitive
    var pattern1 = new RegExp("^\s*"+strAkt+".*$","i");
      if (pattern1.test(this.oSelect.childNodes[selectedIndex].innerHTML))
        arrWords[iWord] = this.oSelect.childNodes[selectedIndex].innerHTML; // vlastni doplneni
  }

  // a znovu vlozim do pole
  this.oText.value = arrWords.join(";");
  this._resetChange(this.oText.value);
  
  // v pripade, ze uzivatel jeste neprovedl filtrovani seznamu vlozenim 
  // pocatecniho pismena jmena do oText, je to klikaci typ a neni potreba 
  // mu znovu inicializovat ten seznam, protoze evidentne provadi vyber 
  // ze seznamu
  if (this.arrNames.length != this.oSelect.childNodes.length)
    this._emptySearch();
  
  this._saveValues(arrWords);
}

