diff --git a/jquery_jstree.js b/jquery_jstree.js
index 3369631..d509ffe 100644
--- a/jquery_jstree.js
+++ b/jquery_jstree.js
@@ -15,19 +15,154 @@
                     'default_value' : options.core.data.default_value
                   };
                 }
+
+                // If a search field is being used, move it to the proper
+                // location and configure it to search the appropriate tree.
+                if (options.drupal && options.drupal.search_field_id) {
+                  var $search_field = $('#' + options.drupal.search_field_id, context).once('jstree-search').eq(0);
+                  if ($search_field.length) {
+                    var $throbber = $('<div class="ajax-progress ajax-progress-throbber"><div class="throbber">&nbsp;</div></div>');
+                    var $no_results_message = $('<p class="jstree-search-no-results">' + Drupal.t('No results found.') + '</p>');
+                    var interval_id = false;
+                    var search_is_queued = false;
+                    var search_is_running = false;
+                    var search_running_value = '';
+                    // The field needs to be moved out of the element that will
+                    // have the jsTree behavior applied; otherwise jsTree will
+                    // just remove it from the DOM.
+                    $search_field.insertBefore(tree)
+                      // Run a debounced search query so that if searches come
+                      // in more often than every 250 ms, the previous search
+                      // is superseded by the new one. This is based on the
+                      // code example at https://www.jstree.com/plugins/ but
+                      // with several modifications to make it more robust (for
+                      // example, to handle the case where the search Ajax
+                      // request itself takes longer than 250 ms).
+                      .on('input', function () {
+                        // Find the text that is being searched for the current
+                        // iteration of this function.
+                        var search_value = $search_field.val();
+                        // Treat single-character searches like empty searches.
+                        // (Searching for a single character is unlikely to be
+                        // useful - most likely if it happens it's because the
+                        // user paused after typing the first character of a
+                        // longer search string - and is likely to return a
+                        // large number of results which can be slow and hold
+                        // up future searches from happening.)
+                        if (search_value.length == 1) {
+                          search_value = '';
+                        }
+                        // If a new search comes in while an old one is still
+                        // waiting to run, clear the old one.
+                        if (interval_id) {
+                          clearInterval(interval_id);
+                        }
+                        // Attempt to run the search on a regular basis until
+                        // it is allowed to proceed.
+                        interval_id = setInterval(function () {
+                          // If another search is running, queue this one to
+                          // run next.
+                          if (search_is_running) {
+                            search_is_queued = true;
+                          }
+                          // Otherwise run this search now.
+                          else {
+                            search_is_running = true;
+                            search_running_value = search_value;
+                            search_is_queued = false;
+                            // Searching can sometimes take a while, so hide
+                            // the old results and add a throbber before
+                            // starting the search. Also remove the "no
+                            // results" message (if the previous search
+                            // returned no results).
+                            $no_results_message.remove();
+                            tree.show();
+                            tree.css('opacity', 0);
+                            $throbber.insertAfter($search_field);
+                            tree.jstree(true).search(search_value);
+                            // Prevent attempting to run the same search again.
+                            clearInterval(interval_id);
+                          }
+                        }, 250);
+                      })
+                      // Prevent the enter key in this field from submitting
+                      // the form.
+                      .keydown(function (event) {
+                        if (event.keyCode == 13) {
+                          event.preventDefault();
+                          return false;
+                        }
+                      });
+                    // When the last queued search completes, remove the
+                    // throbber and show the results.
+                    tree.on('search.jstree', function(event, data) {
+                      search_is_running = false;
+                      search_running_value = '';
+                      if (!search_is_queued) {
+                        $throbber.remove();
+                        tree.css('opacity', 1);
+                        // If there are no results, show the "no results"
+                        // message in place of the tree (if the tree were
+                        // shown, jsTree would display all items within it even
+                        // though none of them match the search, which is not
+                        // desirable here).
+                        if (data.nodes.length == 0) {
+                          tree.hide();
+                          $no_results_message.insertAfter($search_field);
+                        }
+                      }
+                    });
+                    // If the last queued search is a search for an empty
+                    // string, reset everything to its original status once the
+                    // search completes.
+                    tree.on('clear_search.jstree', function(event, data) {
+                      search_length = search_running_value.length;
+                      search_is_running = false;
+                      search_running_value = '';
+                      if (!search_is_queued && search_length == 0) {
+                        $throbber.remove();
+                        tree.css('opacity', 1);
+                        tree.show();
+                        $no_results_message.remove();
+                      }
+                    });
+                  }
+                }
+
                 // Build the tree
                 tree.jstree(options);
+
                 var form = tree.parents('form');
                 if (form.length) {
                   // Sync. tree-node selection to hidden fields in parent form.
                   var inputName = tree.data('name');
                   tree.on('changed.jstree', function (e, data) {
-                    form.find('input.selected-node').remove();
+                    tree.nextAll('input.selected-node').remove();
                     for(i = 0, j = data.selected.length; i < j; i++) {
                       var node = data.instance.get_node(data.selected[i]);
+                      // Allow individual tree elements to use a different form
+                      // input name by providing a data-name property of their
+                      // own (for example, this allows custom code to have
+                      // items from two different Drupal fields displayed in
+                      // the same tree).
+                      if (node.li_attr && node.li_attr['data-name']) {
+                        // It would be cleaner to use $node.data('name') here
+                        // (where $node is the full jQuery object obtained by
+                        // passing a parameter to data.instance.get_node()
+                        // above to tell it to return a jQuery object). But
+                        // jsTree seems to have a bug where it only returns the
+                        // jQuery object if the selected element is visible on
+                        // the form (which it might not be, since its parent
+                        // might be currently closed). So this less clean
+                        // method is used instead.
+                        var elementInputName = node.li_attr['data-name'];
+                      }
+                      else {
+                        var elementInputName = inputName;
+                      }
                       $('<input></input>').attr({
                         type: 'hidden',
-                        name: inputName + '[' + i + ']',
+                        name: elementInputName + '[]',
                         value: node.id,
                         'class': 'selected-node'
                       }).insertAfter(tree);
@@ -49,9 +184,11 @@
         });
       }
     },
-    detach: function (context) {
-      // Destroy any (automatically created) jsTree in the detached context
-      if ($.isPlainObject(Drupal.settings.jstree)) {
+    detach: function (context, settings, trigger) {
+      // Destroy any (automatically created) jsTree in the detached context.
+      // Only do this if the tree was actually removed from the DOM, not for
+      // other detach events such as the Ajax "serialize" event.
+      if (trigger == 'unload' && $.isPlainObject(Drupal.settings.jstree)) {
         $.each(Drupal.settings.jstree, function (id, options) {
           if (Drupal.settings.jstree.hasOwnProperty(id)) {
             var tree = $('#'+id, context);
diff --git a/jquery_jstree.module b/jquery_jstree.module
index 276350a..9315fd6 100644
--- a/jquery_jstree.module
+++ b/jquery_jstree.module
@@ -50,6 +50,7 @@ function jquery_jstree_element_info() {
     '#attributes' => array(),
     '#tree_options' => array(),
     '#input' => TRUE,
+    '#value_callback' => 'jquery_jstree_value_callback',
     '#process' => array('ajax_process_form'),
     '#theme_wrappers' => array('form_element'),
   );
@@ -57,6 +58,21 @@ function jquery_jstree_element_info() {
 }
 
 /**
+ * Element value callback for a jstree form element.
+ */
+function jquery_jstree_value_callback($element, $input = FALSE) {
+  // When there is no input, this function must return something other than
+  // NULL, since NULL would cause the form API to use default value handling
+  // (which would make it impossible to remove all selections from a jstree
+  // form element that has already been saved with some items selected).
+  // Therefore, an empty array is used instead. This is similar to what the
+  // form_type_checkbox_value() value callback does in Drupal core.
+  if (!isset($input)) {
+    return array();
+  }
+}
+
+/**
  * Pre-render callback for jsTree element.
  *
  * @param array $element
@@ -74,9 +90,38 @@ function jquery_jstree_element_pre_render($element) {
   if (empty($element['#options']['plugins']) || !is_array($element['#options']['plugins'])) {
     $element['#tree_options']['plugins'] = empty($element['#tree_options']['plugins']) ? array() : $element['#tree_options']['plugins'];
   }
-  // Pass default value to JavaScript settings
-  if (!empty($element['#default_value'])) {
-    $element['#tree_options']['core']['data']['default_value'] = $element['#default_value'];
+  // Add a search field if the search plugin is in use.
+  if (in_array('search', $element['#tree_options']['plugins'], TRUE)) {
+    $element['jstree_search'] = array(
+      // This is based closely on the definition of the 'textfield' element in
+      // system_element_info(), but with several changes (such as setting
+      // #input to FALSE) so that this isn't processed like a normal form
+      // element, and other changes to make it display correctly as a search
+      // field.
+      '#theme' => 'textfield',
+      '#theme_wrappers' => array('form_element'),
+      '#title' => t('Search'),
+      '#title_display' => 'invisible',
+      '#attributes' => array(
+        'placeholder' => t('Type to search, or choose items below'),
+        'class' => array('jstree-search-input'),
+      ),
+      // This uses a similar method for generating the ID as form_builder()
+      // does.
+      '#id' => drupal_html_id('edit-' . implode('-', array_merge($element['#parents'], array('jstree_search')))),
+      '#input' => FALSE,
+      '#size' => 45,
+      '#maxlength' => 128,
+      '#autocomplete_path' => FALSE,
+      '#process' => array(),
+    );
+    // Identify the search field so the client-side code can find it.
+    $element['#tree_options']['drupal']['search_field_id'] = $element['jstree_search']['#id'];
+  }
+  // Pass the element's current value to the JavaScript settings, but don't set
+  // this if it was already set by code that ran earlier.
+  if (!isset($element['#tree_options']['core']['data']['default_value'])) {
+    $element['#tree_options']['core']['data']['default_value'] = $element['#value'];
   }
   // Attach JavaScript settings, used by our behavior to initialize the tree.
   $element['#attached']['js'][] = array(
diff --git a/modules/jstree_taxonomy/jstree_taxonomy.module b/modules/jstree_taxonomy/jstree_taxonomy.module
index 1f0399f..0323d3c 100644
--- a/modules/jstree_taxonomy/jstree_taxonomy.module
+++ b/modules/jstree_taxonomy/jstree_taxonomy.module
@@ -33,6 +33,7 @@ function jstree_taxonomy_field_widget_settings_form($field, $instance) {
       'checkbox' => t('Checkbox'),
       'wholerow' => t('Wholerow'),
       'sort' => t('Sort'),
+      'search' => t('Search'),
     ),
     '#description' => t('This is not a complete list of jsTree plugins but all these can be enabled with simple toggle and further configurations are not required.'),
   );
@@ -63,6 +64,13 @@ function jstree_taxonomy_field_widget_form(&$form, &$form_state, $field, $instan
           'url' => url('taxonomy/jstree/load/' . $vocabulary->vid),
         ),
       ),
+      // These settings will be ignored if the search plugin is not enabled.
+      'search' => array(
+        'show_only_matches' => TRUE,
+        'ajax' => array(
+          'url' => url('taxonomy/jstree/search/' . $vocabulary->vid),
+        ),
+      ),
     ),
   );
   return $element;
@@ -83,6 +91,12 @@ function jstree_taxonomy_validate($element, &$form_state) {
  * Implements hook_menu()
  */
 function jstree_taxonomy_menu() {
+  $items['taxonomy/jstree/search/%taxonomy_vocabulary'] = array(
+    'page callback' => 'jstree_taxonomy_search',
+    'page arguments' => array(3),
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
   $items['taxonomy/jstree/load/%taxonomy_vocabulary'] = array(
     'page callback' => 'jstree_taxonomy_options',
     'page arguments' => array(3, 4),
@@ -93,6 +107,102 @@ function jstree_taxonomy_menu() {
 }
 
 /**
+ * jsTree callback to search a taxonomy vocabulary.
+ *
+ * The function prints JSON output representing the parent terms that need to
+ * be opened to find terms that match the search string.
+ *
+ * @param stdClass $vocabulary
+ *   The taxonomy vocabulary to search within.
+ */
+function jstree_taxonomy_search($vocabulary) {
+  $query = drupal_get_query_parameters();
+  drupal_json_output(jstree_taxonomy_search_result_parents_from_query($vocabulary, $query));
+}
+
+/**
+ * Returns the parent term IDs that need to be opened for a jsTree search.
+ *
+ * @param $vocabulary
+ *   The taxonomy vocabulary object representing the vocabulary that is being
+ *   searched.
+ * @param $query
+ *   An array of jsTree parameters including at least 'str' (which specifies
+ *   the string being searched for). Typically drupal_get_query_parameters()
+ *   should be passed in for this parameter (to use what the jsTree Ajax
+ *   request asked for) but advanced use cases can modify the query before
+ *   passing it in.
+ * @param $results_exist
+ *   (Optional) An already-defined variable that will be altered by reference
+ *   and that can be used to determine if search results exist. After the
+ *   function completes, this will be FALSE if no search results exist for the
+ *   given search string, or TRUE if they do. This can be useful since there is
+ *   no other way to distinguish a search that has no results from one whose
+ *   results are all top-level terms; in both cases there are no parents that
+ *   need to be opened to reveal the results, so the function's return value is
+ *   the same.
+ *
+ * @return
+ *   An array of term IDs that need to be opened to reveal terms underneath
+ *   which appear in the search results.
+ */
+function jstree_taxonomy_search_result_parents_from_query($vocabulary, $query, &$results_exist = NULL) {
+  $tids_to_open = array();
+  $results_exist = FALSE;
+
+  if (isset($query['str'])) {
+    $search_string = trim(drupal_strtolower($query['str']));
+    if ($search_string !== '') {
+      // This query is based on the one in taxonomy_autocomplete(), but done as
+      // an EntityFieldQuery so that it works more accurately in scenarios
+      // where the term title isn't necessarily stored in {taxonomy_term_data}
+      // (for example, if modules such as Taxonomy Revision and CPS are in
+      // use).
+      $entity_query = new EntityFieldQuery();
+      $entity_query->addTag('translatable');
+      $entity_query->addTag('term_access');
+      $result = $entity_query->entityCondition('entity_type', 'taxonomy_term')
+        ->entityCondition('bundle', $vocabulary->machine_name)
+        ->propertyCondition('name', '%' . db_like($search_string) . '%', 'LIKE')
+        ->execute();
+      $tids = isset($result['taxonomy_term']) ? array_keys($result['taxonomy_term']) : array();
+      if ($tids) {
+        $results_exist = TRUE;
+        // Collect the term IDs of all parents that need to be opened to find
+        // each term. The parents are determined from taxonomy_get_tree()
+        // rather than taxonomy_get_parents_all() because this avoids doing a
+        // full entity load for all the terms and consequently has
+        // substantially better performance.
+        $term_parents_by_tid = array();
+        foreach (taxonomy_get_tree($vocabulary->vid) as $term) {
+          $term_parents_by_tid[$term->tid] = array_filter($term->parents);
+        }
+        foreach ($tids as $tid) {
+          if (!empty($term_parents_by_tid[$tid])) {
+            $parents = $term_parents_by_tid[$tid];
+            while ($parents) {
+              // Add each parent to the list of term IDs to open, then add its
+              // parents (if any) to the list of parents to check.
+              $parent = array_shift($parents);
+              $tids_to_open[] = $parent;
+              if (!empty($term_parents_by_tid[$parent])) {
+                $parents = array_merge($parents, $term_parents_by_tid[$parent]);
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  // Return a unique set of term IDs that need to be opened. The array_values()
+  // call is important here, in order to make sure drupal_json_output() doesn't
+  // include array keys in the output (which jsTree will not be able to
+  // interpret).
+  return array_values(array_unique($tids_to_open));
+}
+
+/**
  * jstree options json callback
  *
  * @param $vocabulary
@@ -103,7 +213,27 @@ function jstree_taxonomy_menu() {
  */
 function jstree_taxonomy_options($vocabulary) {
   $query = drupal_get_query_parameters();
+  $options = jstree_taxonomy_options_from_query($vocabulary, $query);
+  drupal_json_output($options);
+}
 
+/**
+ * Returns an array of jsTree options for part of a taxonomy vocabulary tree.
+ *
+ * @param $vocabulary
+ *   The taxonomy vocabulary object.
+ * @param $query
+ *   An array of jsTree parameters including at least 'id' (specifying the
+ *   parent term ID, if any, whose children should be returned) and potentially
+ *   also including 'default_value' (an array of term IDs that are currently
+ *   selected). Typically drupal_get_query_parameters() should be passed in for
+ *   this parameter (to use what the jsTree Ajax request asked for) but
+ *   advanced use cases can modify the query before passing it in.
+ *
+ * @return
+ *   An array of options in the format expected by jsTree.
+ */
+function jstree_taxonomy_options_from_query($vocabulary, $query) {
   // parent is the node of the tree that is about to get opened.
   $parent = 0;
   if (is_numeric($query['id'])) {
@@ -129,12 +259,14 @@ function jstree_taxonomy_options($vocabulary) {
     }
   }
   $options = array();
-  foreach (taxonomy_get_tree($vocabulary->vid, $parent, 1) as $term) {
-    $children = !empty(taxonomy_get_tree($vocabulary->vid, $term->tid, 1));
+  foreach (taxonomy_get_tree($vocabulary->vid, $parent, 1, TRUE) as $term) {
+    $children = taxonomy_get_tree($vocabulary->vid, $term->tid);
+    $has_children = (bool) $children;
+
     $options[] = array(
       'id' => $term->tid,
       'text' => entity_label('taxonomy_term', $term),
-      'children' => $children,
+      'children' => $has_children,
       'state' => array(
         'opened' => isset($parents[$term->tid]),
         'disabled' => FALSE,
@@ -142,5 +274,6 @@ function jstree_taxonomy_options($vocabulary) {
       ),
     );
   }
-  drupal_json_output($options);
+
+  return $options;
 }
