Source: components/input/autocomplete.js


/**
 * Class representing a single AutoComplete component.
 * @namespace MobileAge.Component.AutoComplete
 * @class
 * @name MobileAge.Component.AutoComplete
 * @extends MobileAge.Component
 * @classdesc AutoComplete-Component
 * @param {string} dom Id, css-query or a ElementNode to connect to.
* @param {Object} options
* @param {String} [options.formLabel="Label"] Label text for the input element..
* @param {String} [options.formName=""] id of the input element.
* @param {String} [options.formValue=""] Value inserted in the input element.
* @param {String} [options.formPlaceholder=""] Placeholder text inserted in the input element.
* @param {String} [options.wrapperLabel=""] html of an element to wrap the label element. Usualy used to work with layout css frameworks. Example for using the Bootstrap grid layout <code>&lt;div class="col-md-3"&gt;&lt;/div&gt;</code>.
* @param {String} [options.wrapperInput=""] html of an element to wrap the input element. Usualy used to work with layout css frameworks. Example for using the Bootstrap grid layout <code>&lt;div class="col-md-9"&gt;&lt;/div&gt;</code>.
* @param {String} [options.wrapperList=""] html of an element to wrap the list element. Usualy used to work with layout css frameworks. Example for using the Bootstrap grid layout <code>&lt;div class="col-md-9"&gt;&lt;/div&gt;</code>.
* @param {String} [options.wrapperStatus=""] Name- and Id-Attribute for the component's dom element.
* @param {String} [options.templateLabel=<label for='<%id%>' class='<%bem%>'><%label%></label>] html of an element to wrap the label of the input. Usualy used to work with layout css frameworks. Example for using the Bootstrap grid layout <code>&lt;div class="col-md-9"&gt;&lt;/div&gt;</code>.
* @param {String} [options.statusOkay=""] Name- and Id-Attribute for the component's dom element.
* @param {String} [options.classActive="active"] CSS-class to be added, if active.
* @param {String} [options.minChars=1] Minimum amount of characters, before a request is started
* @param {String} [options.dataLimit=0] Limits the number of results displayed
* @param {String} [options.clientFiltering=false] Specifies, wether the data is requested completely and filtered client side.
* @param {String} [options.clear=true] Specifies, wether the content of the dom element is deleted before adding the autocomplete.
 */

(function() {
  var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
    hasProp = {}.hasOwnProperty,
    indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };

  MobileAge.Component.AutoComplete = (function(superClass) {
    extend(AutoComplete, superClass);

    function AutoComplete(element, options) {
      var defaultOptions;
      this._input = null;
      this._list = null;
      this._active = null;
      defaultOptions = {
        formLabel: 'Label',
        formName: "",
        formValue: "",
        formPlaceholder: "",
        wrapperLabel: "",
        wrapperInput: "",
        wrapperList: "",
        wrapperStatus: "",
        templateLabel: "<label for='<%id%>' class='<%bem%>'><%label%></label>",
        templateListItem: "<%%>",
        templateListItemWrapper: "<li tabindex='0' role='button' data-value='<%%>'><%%></li>",
        statusOkay: "",
        classActive: "active",
        minChars: 1,
        dataLimit: 0,
        clientFiltering: false,
        clear: true
      };
      AutoComplete.__super__.constructor.call(this, element, this._assignOptions(defaultOptions, options));
      this._create();
    }


    /**
     * Creates a AutoComplete-Component
     * @memberOf MobileAge.Component.AutoComplete
     * @function _create
     */

    AutoComplete.prototype._create = function() {
      if (this._isAttached()) {
        if (this._isSingle()) {
          if (this._options.bem) {
            this.bemify(this._domElement, this._options.bemBlock, this._options.bemElement, this._options.bemModifier);
          }
          if (this._options.clear) {
            this.removeAllChildren(this._domElement);
          }

          /*
          x = Array()
          x.label = @_xcreateLabel()
          x.lebal = @_xcreateLabel()
          @xwrap "<h1><%label%> (<%lebal%>)</h1>", x
           */
          this._createLabel();
          this._createInput();
          this._createList();
        } else if (this._isList()) {
          console.log("Error: MobileAge.Component.AutoComplete only available for single DOM-Elements, yet");
        }
      }
    };


    /**
     * Creates a label element for the AutoComplete input
     * @memberof MobileAge.Component.AutoComplete
     * @function _createLabel
     * @param {string} label - Labeltext
     */

    AutoComplete.prototype._xcreateLabel = function(label) {
      var _item;
      _item = Array();
      _item['label'] = label ? label : this._options.formLabel;
      _item['id'] = this._options.formName;
      _item['bem'] = this._options.bem ? this._options.bemBlock + this._options.bem_BE + "label" : "label";
      return this.html2DomElement(this.template(this._options.templateLabel, _item));
    };

    AutoComplete.prototype._createLabel = function(label) {
      var hlabel, wrapper;
      label = label ? label : this._options.formLabel;
      if (label) {
        hlabel = document.createElement("label");
        hlabel.setAttribute("for", this._options.formName);
        hlabel.appendChild(document.createTextNode(label));
        if (this._options.bem) {
          hlabel.className = this._options.bemBlock + this._options.bem_BE + "label";
        }
        if (this._options.wrapperLabel) {
          wrapper = this.wrap(this._options.wrapperLabel, hlabel);
          return this._domElement.appendChild(wrapper);
        } else {
          return this._domElement.appendChild(hlabel);
        }
      }
    };


    /**
     * Creates an input element with AutoComplete features
     * @memberof MobileAge.Component.AutoComplete
     * @function _createInput
     * @param {string} placeholder - placeholder to be displayed inside the input field temporary
     */

    AutoComplete.prototype._createInput = function(placeholder) {
      var that, wrapper;
      that = this;
      this._input = document.createElement("input");
      this._input.setAttribute("id", this._options.formName);
      this._input.setAttribute("name", this._options.formName);
      this._input.setAttribute("value", this._options.formValue);
      this._input.setAttribute("placeholder", this._options.formPlaceholder);
      this._input.setAttribute("autocomplete", "off");
      if (this._options.bem) {
        this._input.classList.add(this._options.bemBlock + this._options.bem_BE + "input");
      }
      this._input.onkeyup = function(e) {
        return that._keyup(e);
      };
      this._input.onkeypress = function(e) {
        return that._keypress(e);
      };
      if (this._options.wrapperInput) {
        wrapper = this.wrap(this._options.wrapperInput, this._input);
        return this._domElement.appendChild(wrapper);
      } else {
        return this._domElement.appendChild(this._input);
      }
    };


    /**
     * Creates a list component showing the results of the autocomplete
     * @memberof MobileAge.Component.AutoComplete
     * @function _createList
     */

    AutoComplete.prototype._createList = function() {
      var hdiv, options, that;
      that = this;
      options = {
        debug: this._options.debug,
        templateListItemWrapper: this._options.templateListItemWrapper,
        templateListItem: this._options.templateListItem,
        bem: this._options.bem,
        bemBlock: this._options.bemBlock,
        bemElement: "list",
        bemModifier: "",
        xhrMethod: this._options.xhrMethod,
        xhrSource: this._options.xhrSource,
        xhrQuery: this._options.xhrQuery,
        dataPath: this._options.dataPath,
        dataKey: this._options.dataKey,
        dataLimit: this._options.dataLimit,
        onSuccess: this._onRequestData,
        clientFiltering: this._options.clientFiltering,
        autorequest: false,
        onSelect: this._select.bind(this)
      };
      options.xhrQuery[this._options.xhrVar] = "";
      if (this._options.wrapperList) {
        hdiv = this.html2DomElement(this._options.wrapperList);
      } else {
        hdiv = document.createElement("div");
      }
      this._list = new MobileAge.Component.List(hdiv, options);
      if (this._options.clientFiltering) {
        this.queryData();
      }
      return this._domElement.appendChild(hdiv);
    };


    /**
     * Called on keyup event
     * @memberof MobileAge.Component.AutoComplete
     * @function _ckeyup
     * @param {Object} event
     */

    AutoComplete.prototype._keyup = function(event) {
      var key, specialKeys;
      key = event.keyCode;

      /*
       * keyCode - key
       *  9 - Tab
       * 13 - Enter
       * 27 - Escape
       * 32 - " "
       * 38 - ArrowUp
       * 37 - ArrowLeft
       */
      specialKeys = [9, 13, 27, 32, 38, 40];
      if (specialKeys.indexOf(key) === -1) {
        if (this._active) {
          this._active.classList.remove(this._options.classActive);
          this._active = null;
        }
        if (this._input.value.length >= this._options.minChars) {
          this._list.setData(this._data);
          if (this._options.clientFiltering) {
            this._list.filterData(this._input.value);
          } else {
            this.addQuery(this._options.xhrVar, this._input.value);
            this.queryData();
          }
        } else {
          this._list.removeItems();
        }
      }
    };


    /**
     * Called on keypress event
     * @memberof MobileAge.Component.AutoComplete
     * @function _keypress
     * @param {Object} event
     */

    AutoComplete.prototype._keypress = function(event) {
      var key, specialKeys;
      key = event.key;
      specialKeys = ["ArrowUp", "ArrowDown", "Enter", " ", "Escape"];
      if (specialKeys.indexOf(key) >= 0) {
        if (this._input.value.length >= this._options.minChars) {
          if (key === "ArrowDown") {
            if (this._active === null) {
              this._active = this._list._listin.firstChild;
            } else {
              this._active.classList.remove(this._options.classActive);
              this._active = this._active.nextSibling;
              if (this._active === null) {
                this._active = this._list._listin.firstChild;
              }
            }
            if (this._active) {
              this._active.classList.add(this._options.classActive);
            }
            return false;
          } else if (key === "ArrowUp") {
            if (this._active === null) {
              this._active = this._list._listin.lastChild;
            } else {
              this._active.classList.remove(this._options.classActive);
              this._active = this._active.previousSibling;
              if (this._active === null) {
                this._active = this._list._listin.lastChild;
              }
            }
            if (this._active) {
              this._active.classList.add(this._options.classActive);
            }
            return false;
          } else if (key === " " || key === "Enter") {
            if (this._active) {
              this._input.value = this._active.attributes["data-value"].value;
              this._active = null;
              this._list.removeItems();
              return false;
            }
          } else if (key === "Escape") {
            if (this._active) {
              this._active.classList.remove(this._options.classActive);
              this._active = null;
            }
            this._list.removeItems();
            return false;
          }
        }
      }
    };


    /**
     * Adds item to the input, if selected
     * @memberof MobileAge.Component.AutoComplete
     * @function _select
     * @param {Object} e - Event
     */

    AutoComplete.prototype._select = function(e) {
      this._input.value = e.target.attributes["data-value"].value;
      this._input.focus();
      this._active = null;
      return this._list.removeItems();
    };


    /**
     * Checks validity of input
     * @memberof MobileAge.Component.AutoComplete
     * @function _validateInput
     * @param {string} placeholder - placeholder to be displayed inside the input field temporary
     */

    AutoComplete.prototype._validateInput = function(data) {
      var ref;
      if (this._input.value) {
        if (ref = this._input.value, indexOf.call(data, ref) >= 0) {
          this._input.classList.remove("invalid");
          return this._input.classList.add("valid");
        } else {
          this._input.classList.remove("valid");
          return this._input.classList.add("invalid");
        }
      } else {
        this._input.classList.remove("invalid");
        return this._input.classList.remove("valid");
      }
    };


    /**
     * Adds a custom xhr-query
     * @memberof MobileAge.Component.AutoComplete
     * @function _addQuery
     * @param {string} placeholder - placeholder to be displayed inside the input field temporary
     */

    AutoComplete.prototype.addQuery = function(param, query) {
      return this._options.xhrQuery[param] = query;
    };


    /**
     * Callback function called when requested data is received
     * @memberof MobileAge.Component.AutoComplete
     * @function _onRequestData
     * @param {object} data - requested data
     */

    AutoComplete.prototype._onRequestData = (function(data) {
      if (data !== void 0) {
        this._parseData(data);
        this._list.setData(this._data);
        if (this._input.value.length >= this._options.minChars) {
          if (this._options.clientFiltering) {
            this._list.filterData(this._input.value);
          } else {
            this._list.filterData("");
          }
        } else {
          this._list.removeItems();
        }
      }
    });


    /**
     * Callback function called when requested data is received
     * @memberof MobileAge.Component.List
     * @function _onRequestData
     * @param {object} data - requested data
    
    _onRequestData: (data) ->
      if data and Array.isArray(data)
        @_data = data
      else
        @_data = JSON.parse(data)
    
       * get defined path within the data object
      if @_options.array_path
        #console.log Array.isArray(@_data[@_options.array_path])
        @_data = @_data[@_options.array_path]
    
      #@_list.appendData(@_data)
      return
    
     * Method 
    _checkSuggestions: () ->
      @_current = @_data.where('( el, i, res, param ) => param.test( el )', new RegExp(@_input.value, 'i'))
      @_list.replaceData @_current
      return
    
     * Method to actually accept a given suggestion
     _accept: (event) ->
       * If the method was called by an event listener,
       * set the active item accordingly
      @_input.value = @_active
      @_input.focus()
      @_resetCurrent()
      #if event
       *  @_active = MobileAge.eventTarget(event, 'li')
       *  @_input.focus()
      #@_input.value = input = @_active.textContent
      #_resetCurrent()
      return
    
     * Method to handle various key events
    _keyupHandler: (event) ->
      which = event.which
      ignoreKeys = [9,13,16,17,18,20,37,39]
      if(@_current)
        console.log @_current.indexOf(@_active)
       * Tab key
      if which == 9 and @_active
        #@_input.value = @_active
        @_resetCurrent()
    
      else if which == 13 and @_active
        @_accept()
    
      else if which == 27
        #@_input.value = input
        @_resetCurrent()
    
       * Up key TODO: fix it!!!
      else if which == 38
        if(@_current.indexOf(@_active) == 0)
          @_input.focus()
        else
          @_selectionHandler (@_current.indexOf(@_active) - 1 + @_current.length) % @_current.length
       * Down key
      else if which == 40
        if(@_current.indexOf(@_active) == @_current.length-1)
          @_input.focus()
        else
        @_selectionHandler (@_current.indexOf(@_active) + 1) % @_current.length
       * Other key, text present -> check data
      else if ignoreKeys.indexOf(which) == -1 and @_input.value
        @_checkSuggestions()
       * Other key (Back/Delete), no text present
      else
        @_resetCurrent()
    
     * Apply active class and set the input element's
     * value according to the selected item
    _selectionHandler: (index) ->
      if Number.isNaN(index)
        @_checkSuggestions()
      if @_current.length
        @_active = @_current[index or 0]
        #@_input.value = @_active
        @_applyActive (index)
    
     * Method to apply the active class
    _applyActive: (index) ->
      @_list.getChildAt(index).focus()
      childcount = @_current.length
      i=0
      while i < childcount
        if( i == index)
          @_list.getChildAt(i).focus()
          @_list.getChildAt(i).setAttribute('class', 'list list__item list__item--selected')
        else
          @_list.getChildAt(i).setAttribute('class', 'list list__item')
        i++
      return
    
     * Method to prevent defaults when pressing certain keys
    _keydownHandler: (event) ->
      which = event.which
      if (which == 9 or which == 13) and @_active or which == 38 or which == 40
        event.preventDefault()
    
     * Method to prevent defaults when pressing certain keys
    _resetCurrent: () ->
      @_list.clearData()
    
     * Method to handle mouse hover suggestions
    _hoverHandler: (event) ->
      target = MobileAge.eventTarget(event, 'li');
       * If the hovered element is a suggestion item,
       * set the input element's value accordingly
      if @_current.indexOf(target.textContent) > -1
        @_input.value = target.textContent
        hovering = true
         * Don't highlight the active item as
         * there's a hover effect anyway
        if @_active
          @_active.classList.remove ACTIVE_CLASS
      else if hovering
        hovering = false
        @_input.value = if @_active then @_active.textContent else input
        if @_active
          @_active.classList.add ACTIVE_CLASS
      if(@_current)
        conso
      
       * Tab key
      if which == 9 and @_active
        #@_input.value = @_active
        @_resetCurrent()
    
      else if which == 13 and @_active
        @_accept()
    
      else if which == 27
        #@_input.value = input
        @_resetCurrent()
     */

    return AutoComplete;

  })(MobileAge.Component);

}).call(this);