Source: src/includes/autocomplete.inc.js

/**
 * Autocomplete global variables. Used to hold onto various global variables
 * needed for an autocomplete.
 */
// The autocomplete text field input selector.
var _theme_autocomplete_input_selector = {};

// The autocomplete remote boolean.
var _theme_autocomplete_remote = {};

// The theme autocomplete variables.
var _theme_autocomplete_variables = {};

// The theme autocomplete variables.
var _theme_autocomplete_success_handlers = {};

/**
 * Themes an autocomplete.
 * @param {Object} variables
 * @return {String}
 */
function theme_autocomplete(variables) {
  try {
    var html = '';

    // We need to have a unique identifier for this autocomplete. If it is a
    // field, use the field name. Otherwise use the id attribute if it is
    // provided or generate a random one. Then finally attach the autocomplete
    // id to the variables so it can be passed along.
    var autocomplete_id = null;
    if (typeof variables.field_info_field !== 'undefined') {
      autocomplete_id = variables.field_info_field.field_name + '_' + variables.delta;
    }
    else if (typeof variables.attributes.id !== 'undefined') {
      autocomplete_id = variables.attributes.id;
    }
    else { autocomplete_id = user_password(); }
    variables.autocomplete_id = autocomplete_id;

    // Hold onto a copy of the variables.
    _theme_autocomplete_variables[autocomplete_id] = {};
    $.extend(true, _theme_autocomplete_variables[autocomplete_id], variables);

    // Are we dealing with a remote data set?
    var remote = false;
    if (variables.remote) { remote = true; }
    variables.remote = remote;
    _theme_autocomplete_remote[autocomplete_id] = variables.remote;

    // Make sure we have an id to use on the list.
    var id = null;
    if (variables.attributes.id) { id = variables.attributes.id; }
    else {
      id = 'autocomplete_' + user_password();
      variables.attributes.id = id;
    }

    // We need a hidden input to hold the value. If a default value
    // was provided by a form element, use it.
    var hidden_attributes = {};
    $.extend(hidden_attributes, variables.attributes);
    if (
      variables.element &&
      typeof variables.element.default_value !== 'undefined'
    ) { hidden_attributes.value = variables.element.default_value; }
    html += theme('hidden', { attributes: hidden_attributes });

    // Now we need an id for the list.
    var list_id = id + '-list';

    // Build the widget variables.
    var widget = {
      attributes: {
        'id': list_id,
        'data-role': 'listview',
        'data-filter': 'true',
        'data-inset': 'true',
        'data-filter-placeholder': '...'
      }
    };

    // Handle a remote data set.
    var js = '';
    if (variables.remote) {
      widget.items = [];
      // We have a remote data set.
      js += '<script type="text/javascript">' +
        '$("#' + list_id + '").on("filterablebeforefilter", function(e, d) { ' +
          '_theme_autocomplete(this, e, d, "' + autocomplete_id + '"); ' +
        '});' +
      '</script>';
    }
    else {
      // Prepare the items then set the data filter reveal attribute.
      widget.items = _theme_autocomplete_prepare_items(variables);
      widget.attributes['data-filter-reveal'] = true;
    }

    // Save a reference to the autocomplete text field input.
    var selector = '#' + drupalgap_get_page_id() +
      ' #' + id + ' + form.ui-filterable' +
      ' input[data-type="search"]';
    js += '<script type="text/javascript">' +
      '_theme_autocomplete_input_selector["' + autocomplete_id + '"] = \'' +
        selector +
      '\';' +
    '</script>';

    // If there was a default value, set it's key title in the autocomplete's
    // text field.
    if (variables.default_value_label) {
      js += drupalgap_jqm_page_event_script_code({
          page_id: drupalgap_get_page_id(),
          jqm_page_event: 'pageshow',
          jqm_page_event_callback:
            '_theme_autocomplete_set_default_value_label',
          jqm_page_event_args: JSON.stringify({
              selector: selector,
              default_value_label: variables.default_value_label
          })
      }, id);
    }

    // Theme the list and add the js to it, then return the html.
    html += theme('item_list', widget);
    html += js;
    return html;
  }
  catch (error) { console.log('theme_autocomplete - ' + error); }
}

/**
 * An internal function used to handle remote data for an autocomplete.
 * @param {Object} list The unordered list that displays the items.
 * @param {Object} e
 * @param {Object} data
 * @param {String} autocomplete_id
 */
function _theme_autocomplete(list, e, data, autocomplete_id) {
  try {
    var autocomplete = _theme_autocomplete_variables[autocomplete_id];
    // Make sure a filter is present.
    if (typeof autocomplete.filter === 'undefined') {
      console.log(
        '_theme_autocomplete - A "filter" was not supplied.'
      );
      return;
    }

    // Make sure a value and/or label has been supplied so we know how to render
    // the items in the autocomplete list.
    var value_provided = typeof autocomplete.value !== 'undefined';
    var label_provided = typeof autocomplete.label !== 'undefined';
    if (!value_provided && !label_provided) {
      console.log(
        '_theme_autocomplete - A "value" and/or "label" was not supplied.'
      );
      return;
    }
    else {
      // We have a value and/or label. If one isn't provided, set it equal to
      // the other.
      if (!value_provided) { autocomplete.value = autocomplete.label; }
      else if (!label_provided) { autocomplete.label = autocomplete.value; }
    }
    // Setup the vars to handle this widget.
    var $ul = $(list),
        $input = $(data.input),
        value = $input.val(),
        html = '';
    // Clear the list.
    $ul.html('');
    // If a value has been input, start the autocomplete search.
    if (value && value.length > 0 && !autocomplete._searching) {

      autocomplete._searching = true;

      // Show the loader icon.
      $ul.html('<li><div class="ui-loader">' +
        '<span class="ui-icon ui-icon-loading"></span>' +
        '</div></li>');
      $ul.listview('refresh');

      // Let's first build the success handler that will place the items into
      // the autocomplete list.
      _theme_autocomplete_success_handlers[autocomplete_id] = function(
        _autocomplete_id, result_items, _wrapped, _child) {
        try {

          autocomplete._searching = false;

          // If there are no results, and then if an empty callback handler was
          // provided, call it.
          // empty callback handler, then call it and return.
          if (result_items.length == 0) {
            if (autocomplete.empty_callback) {
              var fn = window[autocomplete.empty_callback];
              fn(value);
            }
          }
          else {

            // Convert the result into an items array for a list. Each item will
            // be a JSON object with a "value" and "label" properties.
            var items = [];
            var _value = autocomplete.value;
            var _label = autocomplete.label;
            for (var index in result_items) {
                if (!result_items.hasOwnProperty(index)) { continue; }
                var object = result_items[index];
                var _item = null;
                if (_wrapped) { _item = object[_child]; }
                else { _item = object; }
                var item = {
                  value: _item[_value],
                  label: _item[_label]
                };
                items.push(item);
            }

            // Now render the items, add them to list and refresh the list.
            if (items.length != 0) {
              autocomplete.items = items;
              var _items = _theme_autocomplete_prepare_items(autocomplete);
              for (var index in _items) {
                  if (!_items.hasOwnProperty(index)) { continue; }
                  var item = _items[index];
                  html += '<li>' + item + '</li>';
              }
              $ul.html(html);
              $ul.listview('refresh');
              $ul.trigger('updatelayout');
            }
          }

          // Anybody want to act on the completion of the autocomplete?
          if (autocomplete.finish_callback) {
            var fn = window[autocomplete.finish_callback];
            fn(value);
          }

        }
        catch (error) {
          console.log('_theme_autocomplete_success_handlers[' +
            _autocomplete_id +
          '] - ' + error);
        }
      };

      // Depending on the handler, build the path and call the Drupal site for
      // the data. If it's a custom path, see if a handler was provided,
      // otherwise just default to views.
      var handler = null;
      if (autocomplete.custom) {
        if (autocomplete.handler) { handler = autocomplete.handler; }
        else if (autocomplete.field_info_field && autocomplete.field_info_field.settings.handler) {
          handler = autocomplete.field_info_field.settings.handler;
        }
        else { handler = 'views'; }
      }
      else if (autocomplete.field_info_field) {
        handler = autocomplete.field_info_field.settings.handler;
      }
      else { handler = 'views'; }
      switch (handler) {

        // Views (and Organic Groups)
        case 'views':
          // Prepare the path to the view.
          var path = autocomplete.path + '?' + autocomplete.filter + '=' +
            encodeURIComponent(value);
          // Any extra params to send along?
          if (autocomplete.params) { path += '&' + autocomplete.params; }

          // Retrieve JSON results. Keep in mind, we use this for retrieving
          // Views JSON results and custom hook_menu() path results in Drupal.
          views_datasource_get_view_result(path, {
              success: function(results) {
                // If this was a custom path, don't use a wrapper around the
                // results like the one used by Views Datasource.
                var wrapped = true;
                if (autocomplete.custom) { wrapped = false; }

                // Extract the result items based on the presence of the wrapper
                // or not.
                var result_items = null;
                if (wrapped) { result_items = results[results.view.root]; }
                else { result_items = results; }

                // Finally call the success handler. Note, since we route custom Drupal hook_menu() item JSON page
                // callbacks through this Views handler, we don't attempt to send along the view to the handler. Hack.
                var fn = _theme_autocomplete_success_handlers[autocomplete_id];
                if (results.view) { fn(autocomplete_id, result_items, wrapped, results.view.child); }
                else { fn(autocomplete_id, result_items, wrapped); }
              }
          });
          break;

        // Simple entity selection mode (provided by the entity reference
        // module), use the Index resource for the entity type.
        case 'base':
        case 'og':
          var field_settings =
            autocomplete.field_info_field.settings;
          var index_resource = field_settings.target_type + '_index';
          if (!function_exists(index_resource)) {
            console.log('WARNING - _theme_autocomplete - ' +
              index_resource + '() does not exist!'
            );
            return;
          }
          var options = {
            fields: [autocomplete.value, autocomplete.filter],
            parameters: { },
            parameters_op: { }
          };
          options.parameters[autocomplete.filter] = '%' + value + '%';
          options.parameters_op[autocomplete.filter] = 'like';
          var bundles = entityreference_get_target_bundles(field_settings);
          if (bundles) { options.parameters[entity_get_bundle_name(field_settings.target_type)] = bundles.join(','); }
          window[index_resource](options, {
              success: function(results) {
                _theme_autocomplete_success_handlers[autocomplete_id](autocomplete_id, results, false);
              }
          });
          break;

        // An entity index resource call. Figure out which entity type index
        // to call, and build a default query if one wasn't provided.
        case 'index':
          if (!autocomplete.entity_type) {
            console.log(
              'WARNING - _theme_autocomplete - no entity_type provided'
            );
            return;
          }
          var function_name = autocomplete.entity_type + '_index';
          var fn = window[function_name];
          var query = null;
          if (autocomplete.query) { query = autocomplete.query; }
          else {
            query = {
              parameters: { },
              parameters_op: { }
            };
            var fields = [
              entity_primary_key(autocomplete.entity_type),
              entity_primary_key_title(autocomplete.entity_type)
            ];
            if (autocomplete.entity_type == 'taxonomy_term') {
              if (autocomplete.vid) { query.parameters['vid'] = autocomplete.vid; }
              if (autocomplete.parent) { query.parameters['parent'] = autocomplete.parent; }
            }
            query.fields = fields;
            query.parameters[autocomplete.filter] = '%' + value + '%';
            query.parameters_op[autocomplete.filter] = 'like';
          }
          fn.apply(null, [query, {
              success: function(results) {
                var fn = _theme_autocomplete_success_handlers[autocomplete_id];
                fn(autocomplete_id, results, false, null);
              }
          }]);
          break;

        // The default handler...
        default:

          // If we made it this far, and don't have a handler, then warn the
          // developer.
          if (!handler) {
            console.log('WARNING - _theme_autocomplete - no handler provided');
            return;
          }

          break;

      }

    }
    else {
      // The autocomplete text field was emptied, clear out the hidden value.
      $('#' + autocomplete.id).val('');
    }
  }
  catch (error) { console.log('_theme_autocomplete - ' + error); }
}

/**
 * An internal function used to prepare the items for an autocomplete list.
 * @param {Object} variables
 * @return {*}
 */
function _theme_autocomplete_prepare_items(variables) {
  try {
    // Make sure we have an items array.
    var items = [];
    if (variables.items) { items = variables.items; }

    // Prepare the items, and return them.
    var _items = [];
    if (items.length > 0) {
      for (var index in items) {
          if (!items.hasOwnProperty(index)) { continue; }
          var item = items[index];
          var value = '';
          var label = '';
          if (typeof item === 'string') {
            value = item;
            label = item;
          }
          else {
            value = item.value;
            label = item.label;
          }
          var options = {
            attributes: {
              value: value,
              onclick: '_theme_autocomplete_click(\'' +
                variables.attributes.id +
              '\', this, \'' + variables.autocomplete_id + '\')'
            }
          };
          var _item = l(label, null, options);
          _items.push(_item);
      }
    }
    return _items;
  }
  catch (error) { console.log('_theme_autocomplete_prepare_items - ' + error); }
}

/**
 * An internal function used to handle clicks on items in autocomplete results.
 * @param {String} id The id of the hidden input that holds the value.
 * @param {Object} item The list item anchor that was just clicked.
 * @param {String} autocomplete_id
 */
function _theme_autocomplete_click(id, item, autocomplete_id) {
  try {
    // Set the hidden input with the value, and the text field with the text.
    var list_id = id + '-list';
    $('#' + id).val($(item).attr('value'));
    $(_theme_autocomplete_input_selector[autocomplete_id]).val($(item).html());
    if (_theme_autocomplete_remote[autocomplete_id]) {
      $('#' + list_id).html('');
    }
    else {
      $('#' + list_id + ' li').addClass('ui-screen-hidden');
      $('#' + list_id).listview('refresh');
    }
    // Now fire the item onclick handler, if one was provided.
    if (
      _theme_autocomplete_variables[autocomplete_id].item_onclick &&
      function_exists(
        _theme_autocomplete_variables[autocomplete_id].item_onclick
      )
    ) {
      var fn =
        window[_theme_autocomplete_variables[autocomplete_id].item_onclick];
      fn(id, $(item));
    }
  }
  catch (error) { console.log('_theme_autocomplete_click - ' + error); }
}

/**
 * Used to set a default value in an autocomplete's text field.
 * @param {Object} options
 */
function _theme_autocomplete_set_default_value_label(options) {
  try {
    setTimeout(function() {
        $(options.selector).val(options.default_value_label).trigger('create');
    }, 250);
  }
  catch (error) {
    console.log('_theme_autocomplete_set_default_value_label - ' + error);
  }
}