// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 
//           (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) 
//  
// Permission is hereby granted, free of charge, to any person obtaining 
// a copy of this software and associated documentation files (the 
// "Software"), to deal in the Software without restriction, including 
// without limitation the rights to use, copy, modify, merge, publish, 
// distribute, sublicense, and/or sell copies of the Software, and to 
// permit persons to whom the Software is furnished to do so, subject to 
// the following conditions: 
//  
// The above copyright notice and this permission notice shall be 
// included in all copies or substantial portions of the Software. 
//  
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
 
Element.collectTextNodesIgnoreClass = function(element, ignoreclass) { 
  var children = $(element).childNodes; 
  var text     = ""; 
  var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i"); 
   
  for (var i = 0; i < children.length; i++) { 
    if(children[i].nodeType==3) { 
      text+=children[i].nodeValue; 
    } else { 
      if((!children[i].className.match(classtest)) && children[i].hasChildNodes()) 
        text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass); 
    } 
  } 
   
  return text; 
} 
 
// Autocompleter.Base handles all the autocompletion functionality  
// that's independent of the data source for autocompletion. This 
// includes drawing the autocompletion menu, observing keyboard 
// and mouse events, and similar. 
// 
// Specific autocompleters need to provide, at the very least,  
// a getUpdatedChoices function that will be invoked every time 
// the text inside the monitored textbox changes. This method  
// should get the text for which to provide autocompletion by 
// invoking this.getEntry(), NOT by directly accessing 
// this.element.value. This is to allow incremental tokenized 
// autocompletion. Specific auto-completion logic (AJAX, etc) 
// belongs in getUpdatedChoices. 
// 
// Tokenized incremental autocompletion is enabled automatically 
// when an autocompleter is instantiated with the 'tokens' option 
// in the options parameter, e.g.: 
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); 
// will incrementally autocomplete with a comma as the token. 
// Additionally, ',' in the above example can be replaced with 
// a token array, e.g. { tokens: new Array (',', '\n') } which 
// enables autocompletion on multiple tokens. This is most  
// useful when one of the tokens is \n (a newline), as it  
// allows smart autocompletion after linebreaks. 
 
var Autocompleter = {} 
Autocompleter.Base = function() {}; 
Autocompleter.Base.prototype = { 
  base_initialize: function(element, update, options) { 
    this.element     = $(element);  
    this.update      = $(update);   
    this.has_focus   = false;  
    this.changed     = false;  
    this.active      = false;  
    this.index       = 0;      
    this.entry_count = 0; 
 
    if (this.setOptions) 
      this.setOptions(options); 
    else 
      this.options = {} 
      
    this.options.tokens       = this.options.tokens || new Array(); 
    this.options.frequency    = this.options.frequency || 0.4; 
    this.options.min_chars    = this.options.min_chars || 1; 
    this.options.onShow       = this.options.onShow ||  
    function(element, update){  
      if(!update.style.position || update.style.position=='absolute') { 
        update.style.position = 'absolute'; 
          var offsets = Position.cumulativeOffset(element); 
          update.style.left = offsets[0] + 'px'; 
          update.style.top  = (offsets[1] + element.offsetHeight) + 'px'; 
          update.style.width = element.offsetWidth + 'px'; 
      } 
      new Effect.Appear(update,{duration:0.15}); 
    }; 
    this.options.onHide = this.options.onHide ||  
    function(element, update){ new Effect.Fade(update,{duration:0.15}) }; 
     
    if(this.options.indicator) 
      this.indicator = $(this.options.indicator); 
 
    if (typeof(this.options.tokens) == 'string')  
      this.options.tokens = new Array(this.options.tokens); 
        
    this.observer = null; 
     
    Element.hide(this.update); 
     
    Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); 
    Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); 
  }, 
 
  show: function() { 
    if(this.update.style.display=='none') this.options.onShow(this.element, this.update); 
    if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && this.update.style.position=='absolute') { 
      new Insertion.After(this.update,  
       '<iframe id="' + this.update.id + '_iefix" '+ 
       'style="display:none;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' + 
       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>'); 
      this.iefix = $(this.update.id+'_iefix'); 
    } 
    if(this.iefix) { 
      Position.clone(this.update, this.iefix); 
      this.iefix.style.zIndex = 1; 
      this.update.style.zIndex = 2; 
      Element.show(this.iefix); 
    } 
  }, 
   
  hide: function() { 
    if(this.update.style.display=='') this.options.onHide(this.element, this.update); 
    if(this.iefix) Element.hide(this.iefix); 
  }, 
   
  startIndicator: function() { 
    if(this.indicator) Element.show(this.indicator); 
  }, 
   
  stopIndicator: function() { 
    if(this.indicator) Element.hide(this.indicator); 
  }, 
 
  onKeyPress: function(event) { 
    if(this.active) 
      switch(event.keyCode) { 
       case Event.KEY_TAB: 
       case Event.KEY_RETURN: 
         this.select_entry(); 
         Event.stop(event); 
       case Event.KEY_ESC: 
         this.hide(); 
         this.active = false; 
         return; 
       case Event.KEY_LEFT: 
       case Event.KEY_RIGHT: 
         return; 
       case Event.KEY_UP: 
         this.mark_previous(); 
         this.render(); 
         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); 
         return; 
       case Event.KEY_DOWN: 
         this.mark_next(); 
         this.render(); 
         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); 
         return; 
      } 
     else  
      if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)  
        return; 
     
    this.changed = true; 
    this.has_focus = true; 
     
    if(this.observer) clearTimeout(this.observer); 
      this.observer =  
        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); 
  }, 
   
  onHover: function(event) { 
    var element = Event.findElement(event, 'LI'); 
    if(this.index != element.autocompleteIndex)  
    { 
        this.index = element.autocompleteIndex; 
        this.render(); 
    } 
    Event.stop(event); 
  }, 
   
  onClick: function(event) { 
    var element = Event.findElement(event, 'LI'); 
    this.index = element.autocompleteIndex; 
    this.select_entry(); 
    Event.stop(event); 
  }, 
   
  onBlur: function(event) { 
    // needed to make click events working 
    setTimeout(this.hide.bind(this), 250); 
    this.has_focus = false; 
    this.active = false;      
  },  
   
  render: function() { 
    if(this.entry_count > 0) { 
      for (var i = 0; i < this.entry_count; i++) 
        this.index==i ?  
          Element.addClassName(this.get_entry(i),"selected") :  
          Element.removeClassName(this.get_entry(i),"selected"); 
         
      if(this.has_focus) {  
        if(this.get_current_entry().scrollIntoView)  
          this.get_current_entry().scrollIntoView(false); 
         
        this.show(); 
        this.active = true; 
      } 
    } else this.hide(); 
  }, 
   
  mark_previous: function() { 
    if(this.index > 0) this.index-- 
      else this.index = this.entry_count-1; 
  }, 
   
  mark_next: function() { 
    if(this.index < this.entry_count-1) this.index++ 
      else this.index = 0; 
  }, 
   
  get_entry: function(index) { 
    return this.update.firstChild.childNodes[index]; 
  }, 
   
  get_current_entry: function() { 
    return this.get_entry(this.index); 
  }, 
   
  select_entry: function() { 
    this.active = false; 
    value = Element.collectTextNodesIgnoreClass(this.get_current_entry(), 'informal').unescapeHTML(); 
    this.updateElement(value); 
    this.element.focus(); 
  }, 
 
  updateElement: function(value) { 
    var last_token_pos = this.findLastToken(); 
    if (last_token_pos != -1) { 
      var new_value = this.element.value.substr(0, last_token_pos + 1); 
      var whitespace = this.element.value.substr(last_token_pos + 1).match(/^\s+/); 
      if (whitespace) 
        new_value += whitespace[0]; 
      this.element.value = new_value + value; 
    } else { 
      this.element.value = value; 
    }  
  }, 
   
  updateChoices: function(choices) { 
    if(!this.changed && this.has_focus) { 
      this.update.innerHTML = choices; 
      Element.cleanWhitespace(this.update); 
      Element.cleanWhitespace(this.update.firstChild); 
 
      if(this.update.firstChild && this.update.firstChild.childNodes) { 
        this.entry_count =  
          this.update.firstChild.childNodes.length; 
        for (var i = 0; i < this.entry_count; i++) { 
          entry = this.get_entry(i); 
          entry.autocompleteIndex = i; 
          this.addObservers(entry); 
        } 
      } else {  
        this.entry_count = 0; 
      } 
       
      this.stopIndicator(); 
       
      this.index = 0; 
      this.render(); 
    } 
  }, 
 
  addObservers: function(element) { 
    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); 
    Event.observe(element, "click", this.onClick.bindAsEventListener(this)); 
  }, 
 
  onObserverEvent: function() { 
    this.changed = false;    
    if(this.getEntry().length>=this.options.min_chars) { 
      this.startIndicator(); 
      this.getUpdatedChoices(); 
    } else { 
      this.active = false; 
      this.hide(); 
    } 
  }, 
 
  getEntry: function() { 
    var token_pos = this.findLastToken(); 
    if (token_pos != -1) 
      var ret = this.element.value.substr(token_pos + 1).replace(/^\s+/,'').replace(/\s+$/,''); 
    else 
      var ret = this.element.value; 
     
    return /\n/.test(ret) ? '' : ret; 
  }, 
 
  findLastToken: function() { 
    var last_token_pos = -1; 
 
    for (var i=0; i<this.options.tokens.length; i++) { 
      var this_token_pos = this.element.value.lastIndexOf(this.options.tokens[i]); 
      if (this_token_pos > last_token_pos) 
        last_token_pos = this_token_pos; 
    } 
    return last_token_pos; 
  } 
} 
 
Ajax.Autocompleter = Class.create(); 
Ajax.Autocompleter.prototype = Object.extend(new Autocompleter.Base(),  
Object.extend(new Ajax.Base(), { 
  initialize: function(element, update, url, options) { 
          this.base_initialize(element, update, options); 
    this.options.asynchronous  = true; 
    this.options.onComplete    = this.onComplete.bind(this) 
    this.options.method        = 'post'; 
    this.options.defaultParams = this.options.parameters || null; 
    this.url                   = url; 
  }, 
   
  getUpdatedChoices: function() { 
    entry = encodeURIComponent(this.element.name) + '=' +  
      encodeURIComponent(this.getEntry()); 
       
    this.options.parameters = this.options.callback ? 
      this.options.callback(this.element, entry) : entry; 
         
    if(this.options.defaultParams)  
      this.options.parameters += '&' + this.options.defaultParams; 
     
    new Ajax.Request(this.url, this.options); 
  }, 
   
  onComplete: function(request) { 
    this.updateChoices(request.responseText); 
  } 
 
})); 
 
// The local array autocompleter. Used when you'd prefer to 
// inject an array of autocompletion options into the page, rather 
// than sending out Ajax queries, which can be quite slow sometimes. 
// 
// The constructor takes four parameters. The first two are, as usual, 
// the id of the monitored textbox, and id of the autocompletion menu. 
// The third is the array you want to autocomplete from, and the fourth 
// is the options block. 
// 
// Extra local autocompletion options: 
// - choices - How many autocompletion choices to offer 
// 
// - partial_search - If false, the autocompleter will match entered 
//                    text only at the beginning of strings in the  
//                    autocomplete array. Defaults to true, which will 
//                    match text at the beginning of any *word* in the 
//                    strings in the autocomplete array. If you want to 
//                    search anywhere in the string, additionally set 
//                    the option full_search to true (default: off). 
// 
// - full_search - Search anywhere in autocomplete array strings. 
// 
// - partial_chars - How many characters to enter before triggering 
//                   a partial match (unlike min_chars, which defines 
//                   how many characters are required to do any match 
//                   at all). Defaults to 2. 
// 
// - ignore_case - Whether to ignore case when autocompleting. 
//                 Defaults to true. 
// 
// It's possible to pass in a custom function as the 'selector'  
// option, if you prefer to write your own autocompletion logic. 
// In that case, the other options above will not apply unless 
// you support them. 
 
Autocompleter.Local = Class.create(); 
Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { 
  initialize: function(element, update, array, options) { 
    this.base_initialize(element, update, options); 
    this.options.array = array; 
  }, 
 
  getUpdatedChoices: function() { 
    this.updateChoices(this.options.selector(this)); 
  }, 
 
  setOptions: function(options) { 
    this.options = Object.extend({ 
      choices: 10, 
      partial_search: true, 
      partial_chars: 2, 
      ignore_case: true, 
      full_search: false, 
      selector: function(instance) { 
        var ret       = new Array(); // Beginning matches 
        var partial   = new Array(); // Inside matches 
        var entry     = instance.getEntry(); 
        var count     = 0; 
         
        for (var i = 0; i < instance.options.array.length &&   
            ret.length < instance.options.choices ; i++) {  
          var elem = instance.options.array[i]; 
          var found_pos = instance.options.ignore_case ?  
            elem.toLowerCase().indexOf(entry.toLowerCase()) :  
            elem.indexOf(entry); 
 
          while (found_pos != -1) { 
            if (found_pos == 0 && elem.length != entry.length) {  
              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +  
                elem.substr(entry.length) + "</li>"); 
              break; 
            } else if (entry.length >= instance.options.partial_chars &&  
              instance.options.partial_search && found_pos != -1) { 
              if (instance.options.full_search || /\s/.test(elem.substr(found_pos-1,1))) { 
                partial.push("<li>" + elem.substr(0, found_pos) + "<strong>" + 
                  elem.substr(found_pos, entry.length) + "</strong>" + elem.substr( 
                  found_pos + entry.length) + "</li>"); 
                break; 
              } 
            } 
 
            found_pos = instance.options.ignore_case ?  
              elem.toLowerCase().indexOf(entry.toLowerCase(), found_pos + 1) :  
              elem.indexOf(entry, found_pos + 1); 
 
          } 
        } 
        if (partial.length) 
          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) 
        return "<ul>" + ret.join('') + "</ul>"; 
      } 
    }, options || {}); 
  } 
}); 
