diff --git a/reviewboard/admin/form_widgets.py b/reviewboard/admin/form_widgets.py
index 3a38b1f7328c30d1ad2a686c84ffc76d2ae7fa37..86e8873dae804f48fad9ab66064c2176cc87d93d 100644
--- a/reviewboard/admin/form_widgets.py
+++ b/reviewboard/admin/form_widgets.py
@@ -3,16 +3,18 @@
 from __future__ import unicode_literals
 
 from django.contrib.auth.models import User
-from django.forms.widgets import HiddenInput
 from django.template.loader import render_to_string
 from django.utils import six
 from django.utils.encoding import force_text
 from django.utils.safestring import mark_safe
+from djblets.forms.widgets import RelatedObjectWidget
 
 from reviewboard.avatars import avatar_services
+from reviewboard.reviews.models import Group
+from reviewboard.scmtools.models import Repository
 
 
-class RelatedUserWidget(HiddenInput):
+class RelatedUserWidget(RelatedObjectWidget):
     """A form widget to allow people to select one or more User relations.
 
     It's not unheard of to have a server with thousands or tens of thousands of
@@ -25,29 +27,6 @@ class RelatedUserWidget(HiddenInput):
     in the list, as well as interactive search and filtering.
     """
 
-    # We inherit from HiddenInput in order for the superclass to render a
-    # hidden <input> element, but the siteconfig field template special cases
-    # when ``is_hidden`` is True. Setting it to False still gives us the
-    # rendering and data handling we want but renders fieldset fields
-    # correctly.
-    is_hidden = False
-
-    def __init__(self, local_site_name=None, multivalued=True):
-        """Initalize the RelatedUserWidget.
-
-        Args:
-            local_site_name (unicode, optional):
-                The name of the LocalSite where the widget is being rendered.
-
-            multivalued (bool, optional):
-                Whether or not the widget should allow selecting multiple
-                values.
-        """
-        super(RelatedUserWidget, self).__init__()
-
-        self.local_site_name = local_site_name
-        self.multivalued = multivalued
-
     def render(self, name, value, attrs=None):
         """Render the widget.
 
@@ -55,10 +34,10 @@ class RelatedUserWidget(HiddenInput):
             name (unicode):
                 The name of the field.
 
-            value (list or None):
+            value (list):
                 The current value of the field.
 
-            attrs (dict):
+            attrs (dict, optional):
                 Attributes for the HTML element.
 
         Returns:
@@ -106,15 +85,14 @@ class RelatedUserWidget(HiddenInput):
 
             user_data.append(data)
 
-        extra_html = render_to_string('admin/related_user_widget.html', {
+        return mark_safe(render_to_string('admin/related_user_widget.html', {
+            'input_html': mark_safe(input_html),
             'input_id': final_attrs['id'],
             'local_site_name': self.local_site_name,
             'multivalued': self.multivalued,
             'use_avatars': use_avatars,
             'users': user_data,
-        })
-
-        return mark_safe(input_html + extra_html)
+        }))
 
     def value_from_datadict(self, data, files, name):
         """Unpack the field's value from a datadict.
@@ -146,3 +124,208 @@ class RelatedUserWidget(HiddenInput):
             return value
         else:
             return None
+
+
+class RelatedRepositoryWidget(RelatedObjectWidget):
+    """A form widget allowing people to select one or more Repository objects.
+
+    This widget offers both the ability to see which repositories are already
+    in the list, as well as interactive search and filtering.
+    """
+
+    def render(self, name, value, attrs=None):
+        """Render the widget.
+
+        Args:
+            name (unicode):
+                The name of the field.
+
+            value (list):
+                The current value of the field.
+
+            attrs (dict, optional):
+                Attributes for the HTML element.
+
+        Returns:
+            django.utils.safestring.SafeText:
+            The rendered HTML.
+        """
+        if value:
+            if not self.multivalued:
+                value = [value]
+
+            value = [v for v in value if v]
+            input_value = ','.join(force_text(v) for v in value)
+            existing_repos = (
+                Repository.objects
+                .filter(pk__in=value)
+                .order_by('name')
+            )
+        else:
+            input_value = None
+            existing_repos = []
+
+        final_attrs = self.build_attrs(attrs, name=name)
+
+        input_html = super(RelatedRepositoryWidget, self).render(
+            name, input_value, attrs)
+
+        repo_data = [
+            {
+                'id': repo.pk,
+                'name': repo.name,
+            }
+            for repo in existing_repos
+        ]
+
+        return mark_safe(render_to_string('admin/related_repo_widget.html', {
+            'input_html': mark_safe(input_html),
+            'input_id': final_attrs['id'],
+            'local_site_name': self.local_site_name,
+            'multivalued': self.multivalued,
+            'repos': repo_data,
+        }))
+
+    def value_from_datadict(self, data, files, name):
+        """Unpack the field's value from a datadict.
+
+        Args:
+            data (dict):
+                The form's data.
+
+            files (dict):
+                The form's files.
+
+            name (unicode):
+                The name of the field.
+
+        Returns:
+            list:
+            The list of IDs of
+            :py:class:`~reviewboard.scmtools.models.Repository` objects.
+        """
+        value = data.get(name)
+
+        if self.multivalued:
+            if isinstance(value, list):
+                return value
+            elif isinstance(value, six.string_types):
+                return [v for v in value.split(',') if v]
+            else:
+                return None
+        elif value:
+            return value
+        else:
+            return None
+
+
+class RelatedGroupWidget(RelatedObjectWidget):
+    """A form widget allowing people to select one or more Group objects.
+
+    This widget offers both the ability to see which groups are already in the
+    list, as well as interactive search and filtering.
+    """
+
+    def __init__(self, invite_only=False, *args, **kwargs):
+        """Initialize the RelatedGroupWidget.
+
+        Args:
+            invite_only (bool, optional):
+                Whether or not to display groups that are invite-only.
+
+            *args (tuple):
+                Positional arguments to pass to the handler.
+
+            **kwargs (dict):
+                Keyword arguments to pass to the handler.
+        """
+        super(RelatedGroupWidget, self).__init__(*args, **kwargs)
+        self.invite_only = invite_only
+
+    def render(self, name, value, attrs=None):
+        """Render the widget.
+
+        Args:
+            name (unicode):
+                The name of the field.
+
+            value (list):
+                The current value of the field.
+
+            attrs (dict, optional):
+                Attributes for the HTML element.
+
+        Returns:
+            django.utils.safestring.SafeText:
+            The rendered HTML.
+        """
+        if value:
+            if not self.multivalued:
+                value = [value]
+
+            value = [v for v in value if v]
+            input_value = ','.join(force_text(v) for v in value)
+            existing_groups = (
+                Group.objects
+                .filter(pk__in=value)
+                .order_by('name')
+            )
+        else:
+            input_value = None
+            existing_groups = []
+
+        final_attrs = self.build_attrs(attrs, name=name)
+
+        input_html = super(RelatedGroupWidget, self).render(
+            name, input_value, attrs)
+
+        group_data = []
+
+        for group in existing_groups:
+            data = {
+                'name': group.name,
+                'display_name': group.display_name,
+                'id': group.pk,
+            }
+
+            group_data.append(data)
+
+        return mark_safe(render_to_string('admin/related_group_widget.html', {
+            'input_html': mark_safe(input_html),
+            'input_id': final_attrs['id'],
+            'local_site_name': self.local_site_name,
+            'multivalued': self.multivalued,
+            'groups': group_data,
+            'invite_only': self.invite_only,
+        }))
+
+    def value_from_datadict(self, data, files, name):
+        """Unpack the field's value from a datadict.
+
+        Args:
+            data (dict):
+                The form's data.
+
+            files (dict):
+                The form's files.
+
+            name (unicode):
+                The name of the field.
+
+        Returns:
+            list:
+            The list of PKs of Group objects.
+        """
+        value = data.get(name)
+
+        if self.multivalued:
+            if isinstance(value, list):
+                return value
+            elif isinstance(value, six.string_types):
+                return [v for v in value.split(',') if v]
+            else:
+                return None
+        elif value:
+            return value
+        else:
+            return None
diff --git a/reviewboard/admin/tests.py b/reviewboard/admin/tests.py
index af16ac02cfc63028ac4db4dc621583960fdc82a8..c4d401215b0699824d1fb087089cc3f06732389e 100644
--- a/reviewboard/admin/tests.py
+++ b/reviewboard/admin/tests.py
@@ -4,19 +4,25 @@ import os
 import shutil
 import tempfile
 
+from django import forms
 from django.conf import settings
+from django.contrib.auth.models import User
 from django.forms import ValidationError
 from django.utils.encoding import force_str
 from djblets.siteconfig.models import SiteConfiguration
 
 from reviewboard.admin import checks
+from reviewboard.admin.form_widgets import (RelatedGroupWidget,
+                                            RelatedRepositoryWidget,
+                                            RelatedUserWidget)
 from reviewboard.admin.forms import SearchSettingsForm
 from reviewboard.admin.validation import validate_bug_tracker
-from reviewboard.admin.widgets import (Widget,
-                                       primary_widgets,
+from reviewboard.admin.widgets import (Widget, primary_widgets,
                                        register_admin_widget,
                                        secondary_widgets,
                                        unregister_admin_widget)
+from reviewboard.reviews.models import Group
+from reviewboard.scmtools.models import Repository
 from reviewboard.search import search_backend_registry
 from reviewboard.search.search_backends.base import (SearchBackend,
                                                      SearchBackendForm)
@@ -331,3 +337,438 @@ class WidgetTests(TestCase):
                 unregister_admin_widget(TestSecondaryWidget)
             except KeyError:
                 pass
+
+
+class RelatedUserWidgetTestCase(TestCase):
+    """Unit tests for RelatedUserWidget."""
+
+    fixtures = ['test_users']
+
+    class TestForm(forms.Form):
+        """A Test Form with a field that contains a RelatedUserWidget."""
+        my_multiselect_field = forms.ModelMultipleChoiceField(
+            queryset=User.objects.filter(is_active=True),
+            label=('Default users'),
+            required=False,
+            widget=RelatedUserWidget())
+
+    class LocalSiteTestForm(forms.Form):
+        """A Test Form with a field that contains a RelatedUserWidget.
+
+        The RelatedUserWidget is defined to have a local_site_name."""
+        my_multiselect_field = forms.ModelMultipleChoiceField(
+            queryset=User.objects.filter(is_active=True),
+            label=('Default users'),
+            required=False,
+            widget=RelatedUserWidget(local_site_name='supertest'))
+
+    class SingleValueTestForm(forms.Form):
+        """A Test Form with a field that contains a RelatedUserWidget.
+
+        The RelatedUserWidget is defined as setting multivalued to False."""
+        my_select_field = forms.ModelMultipleChoiceField(
+            queryset=User.objects.filter(is_active=True),
+            label=('Default users'),
+            required=False,
+            widget=RelatedUserWidget(multivalued=False))
+
+    def test_render_empty(self):
+        """Testing RelatedUserWidget.render with no initial data"""
+        my_form = self.TestForm()
+        html = my_form.fields['my_multiselect_field'].widget.render(
+            'Default users',
+            [],
+            {'id': 'default-users'})
+        self.assertHTMLEqual(
+            """<input id="default-users" name="Default users" type="hidden" />
+
+            <script>
+            $(function() {
+                var view = new RB.RelatedUserSelectorView({
+                    $input: $('#default\\u002Dusers'),
+                    initialOptions: [],
+
+                    useAvatars: true,
+                    multivalued: true
+                }).render();
+            });
+            </script>""",
+            html)
+
+    def test_render_with_data(self):
+        """Testing RelatedUserWidget.render with initial data"""
+        my_form = self.TestForm()
+        html = my_form.fields['my_multiselect_field'].widget.render(
+            'Default users',
+            [1, 2, 3],
+            {'id': 'default-users'})
+        self.assertHTMLEqual(
+            """<input id="default-users" name="Default users"
+            type="hidden" value="1,2,3" />
+
+            <script>
+            $(function() {
+                var view = new RB.RelatedUserSelectorView({
+                    $input: $('#default\\u002Dusers'),
+                    initialOptions: [{"username": "admin", "fullname":
+                    "Admin User", "id": 1,
+                    "avatarURL": "https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\\u0026d=mm"},
+                    {"username": "doc", "fullname": "Doc Dwarf", "id": 2,
+                    "avatarURL": "https://secure.gravatar.com/avatar/b0f1ae4342591db2695fb11313114b3e?s=40\\u0026d=mm"},
+                    {"username": "dopey", "fullname": "Dopey Dwarf", "id": 3,
+                    "avatarURL": "https://secure.gravatar.com/avatar/1a0098e6600792ea4f714aa205bf3f2b?s=40\\u0026d=mm"}],
+
+                    useAvatars: true,
+                    multivalued: true
+                }).render();
+            });
+            </script>""",
+            html)
+
+    def test_render_with_local_site(self):
+        """Testing RelatedUserWidget.render with a local site defined"""
+        my_form = self.LocalSiteTestForm()
+        html = my_form.fields['my_multiselect_field'].widget.render(
+            'Default users',
+            [],
+            {'id': 'default-users'})
+        self.assertIn(
+            "localSitePrefix: 's/supertest/',",
+            html)
+
+    def test_value_from_datadict(self):
+        """Testing RelatedUserWidget.value_from_datadict"""
+        my_form = self.TestForm()
+        value = (
+            my_form.fields['my_multiselect_field']
+            .widget
+            .value_from_datadict(
+                {'people': ['1', '2']},
+                {},
+                'people'))
+        self.assertEqual(['1', '2'], value)
+
+    def test_value_from_datadict_single_value(self):
+        """Testing RelatedUserWidget.value_from_datadict with a single value"""
+        my_form = self.SingleValueTestForm()
+        value = (
+            my_form.fields['my_select_field']
+            .widget
+            .value_from_datadict(
+                {'people': ['1']},
+                {},
+                'people'))
+        self.assertEqual(['1'], value)
+
+    def test_value_from_datadict_with_no_data(self):
+        """Testing RelatedUserWidget.value_from_datadict with no data"""
+        my_form = self.TestForm()
+        value = (
+            my_form.fields['my_multiselect_field']
+            .widget
+            .value_from_datadict(
+                {'people': []},
+                {},
+                'people'))
+        self.assertEqual([], value)
+
+    def test_value_from_datadict_with_missing_data(self):
+        """Testing RelatedUserWidget.value_from_datadict with missing data"""
+        my_form = self.TestForm()
+        value = (
+            my_form.fields['my_multiselect_field']
+            .widget
+            .value_from_datadict(
+                {},
+                {},
+                'people'))
+        self.assertEqual(None, value)
+
+
+class RelatedRepositoryWidgetTestCase(TestCase):
+    """Unit tests for RelatedRepositoryWidget."""
+
+    fixtures = ['test_scmtools']
+
+    class TestForm(forms.Form):
+        """A Test Form with a field that contains a RelatedRepositoryWidget."""
+        my_multiselect_field = forms.ModelMultipleChoiceField(
+            queryset=Repository.objects.filter(visible=True).order_by('name'),
+            label=('Repositories'),
+            required=False,
+            widget=RelatedRepositoryWidget())
+
+    class LocalSiteTestForm(forms.Form):
+        """A Test Form with a field that contains a RelatedRepositoryWidget.
+
+        The RelatedRepositoryWidget is defined to have a local_site_name."""
+        my_multiselect_field = forms.ModelMultipleChoiceField(
+            queryset=Repository.objects.filter(visible=True).order_by('name'),
+            label=('Repositories'),
+            required=False,
+            widget=RelatedRepositoryWidget(local_site_name='supertest'))
+
+    class SingleValueTestForm(forms.Form):
+        """A Test Form with a field that contains a RelatedRepositoryWidget.
+
+        RelatedRepositoryWidget is defined as setting multivalued to False."""
+        my_select_field = forms.ModelMultipleChoiceField(
+            queryset=Repository.objects.filter(visible=True).order_by('name'),
+            label=('Repositories'),
+            required=False,
+            widget=RelatedRepositoryWidget(multivalued=False))
+
+    def test_render_empty(self):
+        """Testing RelatedRepositoryWidget.render with no initial data"""
+        my_form = self.TestForm()
+        html = my_form.fields['my_multiselect_field'].widget.render(
+            'Repositories',
+            [],
+            {'id': 'repositories'})
+        self.assertHTMLEqual(
+            """<input id="repositories" name="Repositories" type="hidden" />
+
+            <script>
+            $(function() {
+                var view = new RB.RelatedRepoSelectorView({
+                    $input: $('#repositories'),
+                    initialOptions: [],
+
+                    multivalued: true
+                }).render();
+            });
+            </script>""",
+            html)
+
+    def test_render_with_data(self):
+        """Testing RelatedRepositoryWidget.render with initial data"""
+        test_repo_1 = self.create_repository(name='repo1')
+        test_repo_2 = self.create_repository(name='repo2')
+        test_repo_3 = self.create_repository(name='repo3')
+
+        my_form = self.TestForm()
+        html = my_form.fields['my_multiselect_field'].widget.render(
+            'Repositories',
+            [test_repo_1.pk, test_repo_2.pk, test_repo_3.pk],
+            {'id': 'repositories'})
+        self.assertHTMLEqual(
+            """<input id="repositories" name="Repositories" type="hidden" value="1,2,3" />
+
+            <script>
+            $(function() {
+                var view = new RB.RelatedRepoSelectorView({
+                    $input: $('#repositories'),
+                    initialOptions: [{"id": 1, "name": "repo1"},
+                    {"id": 2, "name": "repo2"},
+                    {"id": 3, "name": "repo3"}],
+
+                    multivalued: true
+                }).render();
+            });
+            </script>""",
+            html)
+
+    def test_render_with_local_site(self):
+        """Testing RelatedRepositoryWidget.render with a local site defined"""
+        my_form = self.LocalSiteTestForm()
+        html = my_form.fields['my_multiselect_field'].widget.render(
+            'Repositories',
+            [],
+            {'id': 'repositories'})
+        self.assertIn(
+            "localSitePrefix: 's/supertest/',",
+            html)
+
+    def test_value_from_datadict(self):
+        """Testing RelatedRepositoryWidget.value_from_datadict"""
+        my_form = self.TestForm()
+        value = (
+            my_form.fields['my_multiselect_field']
+            .widget
+            .value_from_datadict(
+                {'repository': ['1', '2']},
+                {},
+                'repository'))
+        self.assertEqual(['1', '2'], value)
+
+    def test_value_from_datadict_single_value(self):
+        """Testing RelatedRepositoryWidget.value_from_datadict with a single
+        value"""
+        my_form = self.SingleValueTestForm()
+        value = (
+            my_form.fields['my_select_field']
+            .widget
+            .value_from_datadict(
+                {'repository': ['1']},
+                {},
+                'repository'))
+        self.assertEqual(['1'], value)
+
+    def test_value_from_datadict_with_no_data(self):
+        """Testing RelatedRepositoryWidget.value_from_datadict with no data"""
+        my_form = self.TestForm()
+        value = (
+            my_form.fields['my_multiselect_field']
+            .widget
+            .value_from_datadict(
+                {'repository': []},
+                {},
+                'repository'))
+        self.assertEqual([], value)
+
+    def test_value_from_datadict_with_missing_data(self):
+        """Testing RelatedRepositoryWidget.value_from_datadict with missing
+        data"""
+        my_form = self.TestForm()
+        value = (
+            my_form.fields['my_multiselect_field']
+            .widget
+            .value_from_datadict(
+                {},
+                {},
+                'repository'))
+        self.assertEqual(None, value)
+
+
+class RelatedGroupWidgetTestCase(TestCase):
+    """Unit tests for RelatedRepositoryWidget."""
+
+    class TestForm(forms.Form):
+        """A Test Form with a field that contains a RelatedGroupWidget."""
+        my_multiselect_field = forms.ModelMultipleChoiceField(
+            queryset=Group.objects.filter(visible=True).order_by('name'),
+            label=('Default groups'),
+            required=False,
+            widget=RelatedGroupWidget())
+
+    class LocalSiteTestForm(forms.Form):
+        """A Test Form with a field that contains a RelatedGroupWidget.
+
+        The RelatedGroupWidget is defined to have a local_site_name."""
+        my_multiselect_field = forms.ModelMultipleChoiceField(
+            queryset=Group.objects.filter(visible=True).order_by('name'),
+            label=('Default groups'),
+            required=False,
+            widget=RelatedGroupWidget(local_site_name='supertest'))
+
+    class SingleValueTestForm(forms.Form):
+        """A Test Form with a field that contains a RelatedGroupWidget.
+
+        RelatedGroupWidget is defined as setting multivalued to False."""
+        my_select_field = forms.ModelMultipleChoiceField(
+            queryset=Group.objects.filter(visible=True).order_by('name'),
+            label=('Default groups'),
+            required=False,
+            widget=RelatedGroupWidget(multivalued=False))
+
+    def test_render_empty(self):
+        """Testing RelatedGroupWidget.render with no initial data"""
+        my_form = self.TestForm()
+        html = my_form.fields['my_multiselect_field'].widget.render(
+            'Default groups',
+            [],
+            {'id': 'groups'})
+        self.assertHTMLEqual(
+            """<input id="groups" name="Default groups" type="hidden" />
+
+            <script>
+            $(function() {
+                var view = new RB.RelatedGroupSelectorView({
+                    $input: $('#groups'),
+                    initialOptions: [],
+
+                    multivalued: true,
+                    inviteOnly: false
+                }).render();
+            });
+            </script>""",
+            html)
+
+    def test_render_with_data(self):
+        """Testing RelatedGroupWidget.render with initial data"""
+        test_group_1 = self.create_review_group(name='group1')
+        test_group_2 = self.create_review_group(name='group2')
+        test_group_3 = self.create_review_group(name='group3')
+
+        my_form = self.TestForm()
+        html = my_form.fields['my_multiselect_field'].widget.render(
+            'Default groups',
+            [test_group_1.pk, test_group_2.pk, test_group_3.pk],
+            {'id': 'groups'})
+        self.assertHTMLEqual(
+            """<input id="groups" name="Default groups" type="hidden" value="1,2,3" />
+
+            <script>
+            $(function() {
+                var view = new RB.RelatedGroupSelectorView({
+                    $input: $('#groups'),
+                    initialOptions:
+                    [{"display_name": "", "name": "group1", "id": 1},
+                    {"display_name": "", "name": "group2", "id": 2},
+                    {"display_name": "", "name": "group3", "id": 3}],
+
+                    multivalued: true,
+                    inviteOnly: false
+                }).render();
+            });
+            </script>""",
+            html)
+
+    def test_render_with_local_site(self):
+        """Testing RelatedGroupWidget.render with a local site defined"""
+        my_form = self.LocalSiteTestForm()
+        html = my_form.fields['my_multiselect_field'].widget.render(
+            'Default groups',
+            [],
+            {'id': 'groups'})
+        self.assertIn(
+            "localSitePrefix: 's/supertest/',",
+            html)
+
+    def test_value_from_datadict(self):
+        """Testing RelatedGroupWidget.value_from_datadict"""
+        my_form = self.TestForm()
+        value = (
+            my_form.fields['my_multiselect_field']
+            .widget
+            .value_from_datadict(
+                {'groups': ['1', '2']},
+                {},
+                'groups'))
+        self.assertEqual(['1', '2'], value)
+
+    def test_value_from_datadict_single_value(self):
+        """Testing RelatedGroupWidget.value_from_datadict with single value"""
+        my_form = self.SingleValueTestForm()
+        value = (
+            my_form.fields['my_select_field']
+            .widget
+            .value_from_datadict(
+                {'groups': ['1']},
+                {},
+                'groups'))
+        self.assertEqual(['1'], value)
+
+    def test_value_from_datadict_with_no_data(self):
+        """Testing RelatedGroupWidget.value_from_datadict with no data"""
+        my_form = self.TestForm()
+        value = (
+            my_form.fields['my_multiselect_field']
+            .widget
+            .value_from_datadict(
+                {'groups': []},
+                {},
+                'groups'))
+        self.assertEqual([], value)
+
+    def test_value_from_datadict_with_missing_data(self):
+        """Testing RelatedGroupWidget.value_from_datadict with missing data"""
+        my_form = self.TestForm()
+        value = (
+            my_form.fields['my_multiselect_field']
+            .widget
+            .value_from_datadict(
+                {},
+                {},
+                'groups'))
+        self.assertEqual(None, value)
diff --git a/reviewboard/notifications/forms.py b/reviewboard/notifications/forms.py
index 414bb71c7d35f6f218192399f125731c775be977..e945e344aeb29577bd6a69a218210646a9437fd0 100644
--- a/reviewboard/notifications/forms.py
+++ b/reviewboard/notifications/forms.py
@@ -1,11 +1,11 @@
 from __future__ import unicode_literals
 
 from django import forms
-from django.contrib.admin.widgets import FilteredSelectMultiple
 from django.forms.fields import CharField
 from django.utils.translation import ugettext_lazy as _, ugettext
 from djblets.util.compat.django.core.validators import URLValidator
 
+from reviewboard.admin.form_widgets import RelatedRepositoryWidget
 from reviewboard.notifications.models import WebHookTarget
 from reviewboard.scmtools.models import Repository
 
@@ -19,6 +19,12 @@ class WebHookTargetForm(forms.ModelForm):
         widget=forms.widgets.URLInput(attrs={'size': 100})
     )
 
+    repositories = forms.ModelMultipleChoiceField(
+        label=_('Repositories'),
+        required=False,
+        queryset=Repository.objects.filter(visible=True).order_by('name'),
+        widget=RelatedRepositoryWidget())
+
     def clean_extra_data(self):
         """Ensure that extra_data is a valid value.
 
@@ -73,8 +79,6 @@ class WebHookTargetForm(forms.ModelForm):
         model = WebHookTarget
         widgets = {
             'apply_to': forms.widgets.RadioSelect(),
-            'repositories': FilteredSelectMultiple(_('Repositories'),
-                                                   is_stacked=False),
         }
         error_messages = {
             'repositories': {
diff --git a/reviewboard/reviews/forms.py b/reviewboard/reviews/forms.py
index c2c60bdfc9a3a21210371cb999aed3d79e876360..28a71e68ffd966fc0b2602705ffc3f215a21e36e 100644
--- a/reviewboard/reviews/forms.py
+++ b/reviewboard/reviews/forms.py
@@ -3,12 +3,13 @@ from __future__ import unicode_literals
 import re
 
 from django import forms
-from django.contrib.admin.widgets import FilteredSelectMultiple
 from django.contrib.auth.models import User
 from django.core.exceptions import ValidationError
 from django.utils.translation import ugettext, ugettext_lazy as _
 
-from reviewboard.admin.form_widgets import RelatedUserWidget
+from reviewboard.admin.form_widgets import (RelatedGroupWidget,
+                                            RelatedRepositoryWidget,
+                                            RelatedUserWidget)
 from reviewboard.diffviewer import forms as diffviewer_forms
 from reviewboard.diffviewer.models import DiffSet
 from reviewboard.reviews.models import (DefaultReviewer, Group,
@@ -52,7 +53,13 @@ class DefaultReviewerForm(forms.ModelForm):
         help_text=_('The list of repositories to specifically match this '
                     'default reviewer for. If left empty, this will match '
                     'all repositories.'),
-        widget=FilteredSelectMultiple(_("Repositories"), False))
+        widget=RelatedRepositoryWidget())
+
+    groups = forms.ModelMultipleChoiceField(
+        label=_('Default groups'),
+        required=False,
+        queryset=Group.objects.filter(visible=True).order_by('name'),
+        widget=RelatedGroupWidget())
 
     def __init__(self, *args, **kwargs):
         local_site_name = kwargs.pop('local_site_name', None)
diff --git a/reviewboard/scmtools/forms.py b/reviewboard/scmtools/forms.py
index 633497dcf93cb5f60e2440bfa353473727d478f0..fd04b54b10a9f3322da1f21b9b6667a604df081a 100644
--- a/reviewboard/scmtools/forms.py
+++ b/reviewboard/scmtools/forms.py
@@ -4,7 +4,6 @@ import logging
 import sys
 
 from django import forms
-from django.contrib.admin.widgets import FilteredSelectMultiple
 from django.contrib.auth.models import User
 from django.core.exceptions import ValidationError
 from django.forms.widgets import Select
@@ -15,7 +14,8 @@ from django.utils.safestring import mark_safe
 from django.utils.translation import ugettext_lazy as _
 from djblets.util.filesystem import is_exe_in_path
 
-from reviewboard.admin.form_widgets import RelatedUserWidget
+from reviewboard.admin.form_widgets import (RelatedGroupWidget,
+                                            RelatedUserWidget)
 from reviewboard.admin.import_utils import has_module
 from reviewboard.admin.validation import validate_bug_tracker
 from reviewboard.hostingsvcs.errors import (AuthorizationError,
@@ -1436,7 +1436,6 @@ class RepositoryForm(forms.ModelForm):
                                            'autocomplete': 'off'}),
             'username': forms.TextInput(attrs={'size': '30',
                                                'autocomplete': 'off'}),
-            'review_groups': FilteredSelectMultiple(
-                _('review groups with access'), False),
+            'review_groups': RelatedGroupWidget(invite_only=True),
         }
         fields = '__all__'
diff --git a/reviewboard/static/lib/css/selectize.default-0.12.1.css b/reviewboard/static/lib/css/selectize.default-0.12.1.css
deleted file mode 100644
index ffd219c4635b4dda133fb848ad90d7ee1862c5da..0000000000000000000000000000000000000000
--- a/reviewboard/static/lib/css/selectize.default-0.12.1.css
+++ /dev/null
@@ -1,387 +0,0 @@
-/**
- * selectize.default.css (v0.12.1) - Default Theme
- * Copyright (c) 2013–2015 Brian Reavis & contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
- * file except in compliance with the License. You may obtain a copy of the License at:
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under
- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
- * ANY KIND, either express or implied. See the License for the specific language
- * governing permissions and limitations under the License.
- *
- * @author Brian Reavis <brian@thirdroute.com>
- */
-.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {
-  visibility: visible !important;
-  background: #f2f2f2 !important;
-  background: rgba(0, 0, 0, 0.06) !important;
-  border: 0 none !important;
-  -webkit-box-shadow: inset 0 0 12px 4px #ffffff;
-  box-shadow: inset 0 0 12px 4px #ffffff;
-}
-.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {
-  content: '!';
-  visibility: hidden;
-}
-.selectize-control.plugin-drag_drop .ui-sortable-helper {
-  -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
-  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
-}
-.selectize-dropdown-header {
-  position: relative;
-  padding: 5px 8px;
-  border-bottom: 1px solid #d0d0d0;
-  background: #f8f8f8;
-  -webkit-border-radius: 3px 3px 0 0;
-  -moz-border-radius: 3px 3px 0 0;
-  border-radius: 3px 3px 0 0;
-}
-.selectize-dropdown-header-close {
-  position: absolute;
-  right: 8px;
-  top: 50%;
-  color: #303030;
-  opacity: 0.4;
-  margin-top: -12px;
-  line-height: 20px;
-  font-size: 20px !important;
-}
-.selectize-dropdown-header-close:hover {
-  color: #000000;
-}
-.selectize-dropdown.plugin-optgroup_columns .optgroup {
-  border-right: 1px solid #f2f2f2;
-  border-top: 0 none;
-  float: left;
-  -webkit-box-sizing: border-box;
-  -moz-box-sizing: border-box;
-  box-sizing: border-box;
-}
-.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
-  border-right: 0 none;
-}
-.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
-  display: none;
-}
-.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
-  border-top: 0 none;
-}
-.selectize-control.plugin-remove_button [data-value] {
-  position: relative;
-  padding-right: 24px !important;
-}
-.selectize-control.plugin-remove_button [data-value] .remove {
-  z-index: 1;
-  /* fixes ie bug (see #392) */
-  position: absolute;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  width: 17px;
-  text-align: center;
-  font-weight: bold;
-  font-size: 12px;
-  color: inherit;
-  text-decoration: none;
-  vertical-align: middle;
-  display: inline-block;
-  padding: 2px 0 0 0;
-  border-left: 1px solid #0073bb;
-  -webkit-border-radius: 0 2px 2px 0;
-  -moz-border-radius: 0 2px 2px 0;
-  border-radius: 0 2px 2px 0;
-  -webkit-box-sizing: border-box;
-  -moz-box-sizing: border-box;
-  box-sizing: border-box;
-}
-.selectize-control.plugin-remove_button [data-value] .remove:hover {
-  background: rgba(0, 0, 0, 0.05);
-}
-.selectize-control.plugin-remove_button [data-value].active .remove {
-  border-left-color: #00578d;
-}
-.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover {
-  background: none;
-}
-.selectize-control.plugin-remove_button .disabled [data-value] .remove {
-  border-left-color: #aaaaaa;
-}
-.selectize-control {
-  position: relative;
-}
-.selectize-dropdown,
-.selectize-input,
-.selectize-input input {
-  color: #303030;
-  font-family: inherit;
-  font-size: 13px;
-  line-height: 18px;
-  -webkit-font-smoothing: inherit;
-}
-.selectize-input,
-.selectize-control.single .selectize-input.input-active {
-  background: #ffffff;
-  cursor: text;
-  display: inline-block;
-}
-.selectize-input {
-  border: 1px solid #d0d0d0;
-  padding: 8px 8px;
-  display: inline-block;
-  width: 100%;
-  overflow: hidden;
-  position: relative;
-  z-index: 1;
-  -webkit-box-sizing: border-box;
-  -moz-box-sizing: border-box;
-  box-sizing: border-box;
-  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1);
-  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1);
-  -webkit-border-radius: 3px;
-  -moz-border-radius: 3px;
-  border-radius: 3px;
-}
-.selectize-control.multi .selectize-input.has-items {
-  padding: 5px 8px 2px;
-}
-.selectize-input.full {
-  background-color: #ffffff;
-}
-.selectize-input.disabled,
-.selectize-input.disabled * {
-  cursor: default !important;
-}
-.selectize-input.focus {
-  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
-  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
-}
-.selectize-input.dropdown-active {
-  -webkit-border-radius: 3px 3px 0 0;
-  -moz-border-radius: 3px 3px 0 0;
-  border-radius: 3px 3px 0 0;
-}
-.selectize-input > * {
-  vertical-align: baseline;
-  display: -moz-inline-stack;
-  display: inline-block;
-  zoom: 1;
-  *display: inline;
-}
-.selectize-control.multi .selectize-input > div {
-  cursor: pointer;
-  margin: 0 3px 3px 0;
-  padding: 2px 6px;
-  background: #1da7ee;
-  color: #ffffff;
-  border: 1px solid #0073bb;
-}
-.selectize-control.multi .selectize-input > div.active {
-  background: #92c836;
-  color: #ffffff;
-  border: 1px solid #00578d;
-}
-.selectize-control.multi .selectize-input.disabled > div,
-.selectize-control.multi .selectize-input.disabled > div.active {
-  color: #ffffff;
-  background: #d2d2d2;
-  border: 1px solid #aaaaaa;
-}
-.selectize-input > input {
-  display: inline-block !important;
-  padding: 0 !important;
-  min-height: 0 !important;
-  max-height: none !important;
-  max-width: 100% !important;
-  margin: 0 1px !important;
-  text-indent: 0 !important;
-  border: 0 none !important;
-  background: none !important;
-  line-height: inherit !important;
-  -webkit-user-select: auto !important;
-  -webkit-box-shadow: none !important;
-  box-shadow: none !important;
-}
-.selectize-input > input::-ms-clear {
-  display: none;
-}
-.selectize-input > input:focus {
-  outline: none !important;
-}
-.selectize-input::after {
-  content: ' ';
-  display: block;
-  clear: left;
-}
-.selectize-input.dropdown-active::before {
-  content: ' ';
-  display: block;
-  position: absolute;
-  background: #f0f0f0;
-  height: 1px;
-  bottom: 0;
-  left: 0;
-  right: 0;
-}
-.selectize-dropdown {
-  position: absolute;
-  z-index: 10;
-  border: 1px solid #d0d0d0;
-  background: #ffffff;
-  margin: -1px 0 0 0;
-  border-top: 0 none;
-  -webkit-box-sizing: border-box;
-  -moz-box-sizing: border-box;
-  box-sizing: border-box;
-  -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-  -webkit-border-radius: 0 0 3px 3px;
-  -moz-border-radius: 0 0 3px 3px;
-  border-radius: 0 0 3px 3px;
-}
-.selectize-dropdown [data-selectable] {
-  cursor: pointer;
-  overflow: hidden;
-}
-.selectize-dropdown [data-selectable] .highlight {
-  background: rgba(125, 168, 208, 0.2);
-  -webkit-border-radius: 1px;
-  -moz-border-radius: 1px;
-  border-radius: 1px;
-}
-.selectize-dropdown [data-selectable],
-.selectize-dropdown .optgroup-header {
-  padding: 5px 8px;
-}
-.selectize-dropdown .optgroup:first-child .optgroup-header {
-  border-top: 0 none;
-}
-.selectize-dropdown .optgroup-header {
-  color: #303030;
-  background: #ffffff;
-  cursor: default;
-}
-.selectize-dropdown .active {
-  background-color: #f5fafd;
-  color: #495c68;
-}
-.selectize-dropdown .active.create {
-  color: #495c68;
-}
-.selectize-dropdown .create {
-  color: rgba(48, 48, 48, 0.5);
-}
-.selectize-dropdown-content {
-  overflow-y: auto;
-  overflow-x: hidden;
-  max-height: 200px;
-}
-.selectize-control.single .selectize-input,
-.selectize-control.single .selectize-input input {
-  cursor: pointer;
-}
-.selectize-control.single .selectize-input.input-active,
-.selectize-control.single .selectize-input.input-active input {
-  cursor: text;
-}
-.selectize-control.single .selectize-input:after {
-  content: ' ';
-  display: block;
-  position: absolute;
-  top: 50%;
-  right: 15px;
-  margin-top: -3px;
-  width: 0;
-  height: 0;
-  border-style: solid;
-  border-width: 5px 5px 0 5px;
-  border-color: #808080 transparent transparent transparent;
-}
-.selectize-control.single .selectize-input.dropdown-active:after {
-  margin-top: -4px;
-  border-width: 0 5px 5px 5px;
-  border-color: transparent transparent #808080 transparent;
-}
-.selectize-control.rtl.single .selectize-input:after {
-  left: 15px;
-  right: auto;
-}
-.selectize-control.rtl .selectize-input > input {
-  margin: 0 4px 0 -2px !important;
-}
-.selectize-control .selectize-input.disabled {
-  opacity: 0.5;
-  background-color: #fafafa;
-}
-.selectize-control.multi .selectize-input.has-items {
-  padding-left: 5px;
-  padding-right: 5px;
-}
-.selectize-control.multi .selectize-input.disabled [data-value] {
-  color: #999;
-  text-shadow: none;
-  background: none;
-  -webkit-box-shadow: none;
-  box-shadow: none;
-}
-.selectize-control.multi .selectize-input.disabled [data-value],
-.selectize-control.multi .selectize-input.disabled [data-value] .remove {
-  border-color: #e6e6e6;
-}
-.selectize-control.multi .selectize-input.disabled [data-value] .remove {
-  background: none;
-}
-.selectize-control.multi .selectize-input [data-value] {
-  text-shadow: 0 1px 0 rgba(0, 51, 83, 0.3);
-  -webkit-border-radius: 3px;
-  -moz-border-radius: 3px;
-  border-radius: 3px;
-  background-color: #1b9dec;
-  background-image: -moz-linear-gradient(top, #1da7ee, #178ee9);
-  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#1da7ee), to(#178ee9));
-  background-image: -webkit-linear-gradient(top, #1da7ee, #178ee9);
-  background-image: -o-linear-gradient(top, #1da7ee, #178ee9);
-  background-image: linear-gradient(to bottom, #1da7ee, #178ee9);
-  background-repeat: repeat-x;
-  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff1da7ee', endColorstr='#ff178ee9', GradientType=0);
-  -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.2),inset 0 1px rgba(255,255,255,0.03);
-  box-shadow: 0 1px 0 rgba(0,0,0,0.2),inset 0 1px rgba(255,255,255,0.03);
-}
-.selectize-control.multi .selectize-input [data-value].active {
-  background-color: #0085d4;
-  background-image: -moz-linear-gradient(top, #008fd8, #0075cf);
-  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#008fd8), to(#0075cf));
-  background-image: -webkit-linear-gradient(top, #008fd8, #0075cf);
-  background-image: -o-linear-gradient(top, #008fd8, #0075cf);
-  background-image: linear-gradient(to bottom, #008fd8, #0075cf);
-  background-repeat: repeat-x;
-  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff008fd8', endColorstr='#ff0075cf', GradientType=0);
-}
-.selectize-control.single .selectize-input {
-  -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.8);
-  box-shadow: 0 1px 0 rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.8);
-  background-color: #f9f9f9;
-  background-image: -moz-linear-gradient(top, #fefefe, #f2f2f2);
-  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fefefe), to(#f2f2f2));
-  background-image: -webkit-linear-gradient(top, #fefefe, #f2f2f2);
-  background-image: -o-linear-gradient(top, #fefefe, #f2f2f2);
-  background-image: linear-gradient(to bottom, #fefefe, #f2f2f2);
-  background-repeat: repeat-x;
-  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffefefe', endColorstr='#fff2f2f2', GradientType=0);
-}
-.selectize-control.single .selectize-input,
-.selectize-dropdown.single {
-  border-color: #b8b8b8;
-}
-.selectize-dropdown .optgroup-header {
-  padding-top: 7px;
-  font-weight: bold;
-  font-size: 0.85em;
-}
-.selectize-dropdown .optgroup {
-  border-top: 1px solid #f0f0f0;
-}
-.selectize-dropdown .optgroup:first-child {
-  border-top: 0 none;
-}
diff --git a/reviewboard/static/lib/js/README.selectize b/reviewboard/static/lib/js/README.selectize
deleted file mode 100644
index 6a91e6b9f3795479ac8a29a444c8b86d48fa3b6c..0000000000000000000000000000000000000000
--- a/reviewboard/static/lib/js/README.selectize
+++ /dev/null
@@ -1,4 +0,0 @@
-The version of Selectize that's included here (currently 0.12.1) is vanilla
-upstream, but if you update it, you need to look at the padding in
-.related-object-selected inside rb/css/ui/related-object-selector.less
-and verify that it's still correct.
diff --git a/reviewboard/static/lib/js/selectize-0.12.1.js b/reviewboard/static/lib/js/selectize-0.12.1.js
deleted file mode 100644
index e8c0914089a05bac7a4c1e9192af8e93db6a4045..0000000000000000000000000000000000000000
--- a/reviewboard/static/lib/js/selectize-0.12.1.js
+++ /dev/null
@@ -1,3667 +0,0 @@
-/**
- * sifter.js
- * Copyright (c) 2013 Brian Reavis & contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
- * file except in compliance with the License. You may obtain a copy of the License at:
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under
- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
- * ANY KIND, either express or implied. See the License for the specific language
- * governing permissions and limitations under the License.
- *
- * @author Brian Reavis <brian@thirdroute.com>
- */
-
-(function(root, factory) {
-	if (typeof define === 'function' && define.amd) {
-		define('sifter', factory);
-	} else if (typeof exports === 'object') {
-		module.exports = factory();
-	} else {
-		root.Sifter = factory();
-	}
-}(this, function() {
-
-	/**
-	 * Textually searches arrays and hashes of objects
-	 * by property (or multiple properties). Designed
-	 * specifically for autocomplete.
-	 *
-	 * @constructor
-	 * @param {array|object} items
-	 * @param {object} items
-	 */
-	var Sifter = function(items, settings) {
-		this.items = items;
-		this.settings = settings || {diacritics: true};
-	};
-
-	/**
-	 * Splits a search string into an array of individual
-	 * regexps to be used to match results.
-	 *
-	 * @param {string} query
-	 * @returns {array}
-	 */
-	Sifter.prototype.tokenize = function(query) {
-		query = trim(String(query || '').toLowerCase());
-		if (!query || !query.length) return [];
-
-		var i, n, regex, letter;
-		var tokens = [];
-		var words = query.split(/ +/);
-
-		for (i = 0, n = words.length; i < n; i++) {
-			regex = escape_regex(words[i]);
-			if (this.settings.diacritics) {
-				for (letter in DIACRITICS) {
-					if (DIACRITICS.hasOwnProperty(letter)) {
-						regex = regex.replace(new RegExp(letter, 'g'), DIACRITICS[letter]);
-					}
-				}
-			}
-			tokens.push({
-				string : words[i],
-				regex  : new RegExp(regex, 'i')
-			});
-		}
-
-		return tokens;
-	};
-
-	/**
-	 * Iterates over arrays and hashes.
-	 *
-	 * ```
-	 * this.iterator(this.items, function(item, id) {
-	 *    // invoked for each item
-	 * });
-	 * ```
-	 *
-	 * @param {array|object} object
-	 */
-	Sifter.prototype.iterator = function(object, callback) {
-		var iterator;
-		if (is_array(object)) {
-			iterator = Array.prototype.forEach || function(callback) {
-				for (var i = 0, n = this.length; i < n; i++) {
-					callback(this[i], i, this);
-				}
-			};
-		} else {
-			iterator = function(callback) {
-				for (var key in this) {
-					if (this.hasOwnProperty(key)) {
-						callback(this[key], key, this);
-					}
-				}
-			};
-		}
-
-		iterator.apply(object, [callback]);
-	};
-
-	/**
-	 * Returns a function to be used to score individual results.
-	 *
-	 * Good matches will have a higher score than poor matches.
-	 * If an item is not a match, 0 will be returned by the function.
-	 *
-	 * @param {object|string} search
-	 * @param {object} options (optional)
-	 * @returns {function}
-	 */
-	Sifter.prototype.getScoreFunction = function(search, options) {
-		var self, fields, tokens, token_count;
-
-		self        = this;
-		search      = self.prepareSearch(search, options);
-		tokens      = search.tokens;
-		fields      = search.options.fields;
-		token_count = tokens.length;
-
-		/**
-		 * Calculates how close of a match the
-		 * given value is against a search token.
-		 *
-		 * @param {mixed} value
-		 * @param {object} token
-		 * @return {number}
-		 */
-		var scoreValue = function(value, token) {
-			var score, pos;
-
-			if (!value) return 0;
-			value = String(value || '');
-			pos = value.search(token.regex);
-			if (pos === -1) return 0;
-			score = token.string.length / value.length;
-			if (pos === 0) score += 0.5;
-			return score;
-		};
-
-		/**
-		 * Calculates the score of an object
-		 * against the search query.
-		 *
-		 * @param {object} token
-		 * @param {object} data
-		 * @return {number}
-		 */
-		var scoreObject = (function() {
-			var field_count = fields.length;
-			if (!field_count) {
-				return function() { return 0; };
-			}
-			if (field_count === 1) {
-				return function(token, data) {
-					return scoreValue(data[fields[0]], token);
-				};
-			}
-			return function(token, data) {
-				for (var i = 0, sum = 0; i < field_count; i++) {
-					sum += scoreValue(data[fields[i]], token);
-				}
-				return sum / field_count;
-			};
-		})();
-
-		if (!token_count) {
-			return function() { return 0; };
-		}
-		if (token_count === 1) {
-			return function(data) {
-				return scoreObject(tokens[0], data);
-			};
-		}
-
-		if (search.options.conjunction === 'and') {
-			return function(data) {
-				var score;
-				for (var i = 0, sum = 0; i < token_count; i++) {
-					score = scoreObject(tokens[i], data);
-					if (score <= 0) return 0;
-					sum += score;
-				}
-				return sum / token_count;
-			};
-		} else {
-			return function(data) {
-				for (var i = 0, sum = 0; i < token_count; i++) {
-					sum += scoreObject(tokens[i], data);
-				}
-				return sum / token_count;
-			};
-		}
-	};
-
-	/**
-	 * Returns a function that can be used to compare two
-	 * results, for sorting purposes. If no sorting should
-	 * be performed, `null` will be returned.
-	 *
-	 * @param {string|object} search
-	 * @param {object} options
-	 * @return function(a,b)
-	 */
-	Sifter.prototype.getSortFunction = function(search, options) {
-		var i, n, self, field, fields, fields_count, multiplier, multipliers, get_field, implicit_score, sort;
-
-		self   = this;
-		search = self.prepareSearch(search, options);
-		sort   = (!search.query && options.sort_empty) || options.sort;
-
-		/**
-		 * Fetches the specified sort field value
-		 * from a search result item.
-		 *
-		 * @param  {string} name
-		 * @param  {object} result
-		 * @return {mixed}
-		 */
-		get_field = function(name, result) {
-			if (name === '$score') return result.score;
-			return self.items[result.id][name];
-		};
-
-		// parse options
-		fields = [];
-		if (sort) {
-			for (i = 0, n = sort.length; i < n; i++) {
-				if (search.query || sort[i].field !== '$score') {
-					fields.push(sort[i]);
-				}
-			}
-		}
-
-		// the "$score" field is implied to be the primary
-		// sort field, unless it's manually specified
-		if (search.query) {
-			implicit_score = true;
-			for (i = 0, n = fields.length; i < n; i++) {
-				if (fields[i].field === '$score') {
-					implicit_score = false;
-					break;
-				}
-			}
-			if (implicit_score) {
-				fields.unshift({field: '$score', direction: 'desc'});
-			}
-		} else {
-			for (i = 0, n = fields.length; i < n; i++) {
-				if (fields[i].field === '$score') {
-					fields.splice(i, 1);
-					break;
-				}
-			}
-		}
-
-		multipliers = [];
-		for (i = 0, n = fields.length; i < n; i++) {
-			multipliers.push(fields[i].direction === 'desc' ? -1 : 1);
-		}
-
-		// build function
-		fields_count = fields.length;
-		if (!fields_count) {
-			return null;
-		} else if (fields_count === 1) {
-			field = fields[0].field;
-			multiplier = multipliers[0];
-			return function(a, b) {
-				return multiplier * cmp(
-					get_field(field, a),
-					get_field(field, b)
-				);
-			};
-		} else {
-			return function(a, b) {
-				var i, result, a_value, b_value, field;
-				for (i = 0; i < fields_count; i++) {
-					field = fields[i].field;
-					result = multipliers[i] * cmp(
-						get_field(field, a),
-						get_field(field, b)
-					);
-					if (result) return result;
-				}
-				return 0;
-			};
-		}
-	};
-
-	/**
-	 * Parses a search query and returns an object
-	 * with tokens and fields ready to be populated
-	 * with results.
-	 *
-	 * @param {string} query
-	 * @param {object} options
-	 * @returns {object}
-	 */
-	Sifter.prototype.prepareSearch = function(query, options) {
-		if (typeof query === 'object') return query;
-
-		options = extend({}, options);
-
-		var option_fields     = options.fields;
-		var option_sort       = options.sort;
-		var option_sort_empty = options.sort_empty;
-
-		if (option_fields && !is_array(option_fields)) options.fields = [option_fields];
-		if (option_sort && !is_array(option_sort)) options.sort = [option_sort];
-		if (option_sort_empty && !is_array(option_sort_empty)) options.sort_empty = [option_sort_empty];
-
-		return {
-			options : options,
-			query   : String(query || '').toLowerCase(),
-			tokens  : this.tokenize(query),
-			total   : 0,
-			items   : []
-		};
-	};
-
-	/**
-	 * Searches through all items and returns a sorted array of matches.
-	 *
-	 * The `options` parameter can contain:
-	 *
-	 *   - fields {string|array}
-	 *   - sort {array}
-	 *   - score {function}
-	 *   - filter {bool}
-	 *   - limit {integer}
-	 *
-	 * Returns an object containing:
-	 *
-	 *   - options {object}
-	 *   - query {string}
-	 *   - tokens {array}
-	 *   - total {int}
-	 *   - items {array}
-	 *
-	 * @param {string} query
-	 * @param {object} options
-	 * @returns {object}
-	 */
-	Sifter.prototype.search = function(query, options) {
-		var self = this, value, score, search, calculateScore;
-		var fn_sort;
-		var fn_score;
-
-		search  = this.prepareSearch(query, options);
-		options = search.options;
-		query   = search.query;
-
-		// generate result scoring function
-		fn_score = options.score || self.getScoreFunction(search);
-
-		// perform search and sort
-		if (query.length) {
-			self.iterator(self.items, function(item, id) {
-				score = fn_score(item);
-				if (options.filter === false || score > 0) {
-					search.items.push({'score': score, 'id': id});
-				}
-			});
-		} else {
-			self.iterator(self.items, function(item, id) {
-				search.items.push({'score': 1, 'id': id});
-			});
-		}
-
-		fn_sort = self.getSortFunction(search, options);
-		if (fn_sort) search.items.sort(fn_sort);
-
-		// apply limits
-		search.total = search.items.length;
-		if (typeof options.limit === 'number') {
-			search.items = search.items.slice(0, options.limit);
-		}
-
-		return search;
-	};
-
-	// utilities
-	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-	var cmp = function(a, b) {
-		if (typeof a === 'number' && typeof b === 'number') {
-			return a > b ? 1 : (a < b ? -1 : 0);
-		}
-		a = asciifold(String(a || ''));
-		b = asciifold(String(b || ''));
-		if (a > b) return 1;
-		if (b > a) return -1;
-		return 0;
-	};
-
-	var extend = function(a, b) {
-		var i, n, k, object;
-		for (i = 1, n = arguments.length; i < n; i++) {
-			object = arguments[i];
-			if (!object) continue;
-			for (k in object) {
-				if (object.hasOwnProperty(k)) {
-					a[k] = object[k];
-				}
-			}
-		}
-		return a;
-	};
-
-	var trim = function(str) {
-		return (str + '').replace(/^\s+|\s+$|/g, '');
-	};
-
-	var escape_regex = function(str) {
-		return (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
-	};
-
-	var is_array = Array.isArray || ($ && $.isArray) || function(object) {
-		return Object.prototype.toString.call(object) === '[object Array]';
-	};
-
-	var DIACRITICS = {
-		'a': '[aÀÁÂÃÄÅàáâãäåĀāąĄ]',
-		'c': '[cÇçćĆčČ]',
-		'd': '[dđĐďĎ]',
-		'e': '[eÈÉÊËèéêëěĚĒēęĘ]',
-		'i': '[iÌÍÎÏìíîïĪī]',
-		'l': '[lłŁ]',
-		'n': '[nÑñňŇńŃ]',
-		'o': '[oÒÓÔÕÕÖØòóôõöøŌō]',
-		'r': '[rřŘ]',
-		's': '[sŠšśŚ]',
-		't': '[tťŤ]',
-		'u': '[uÙÚÛÜùúûüůŮŪū]',
-		'y': '[yŸÿýÝ]',
-		'z': '[zŽžżŻźŹ]'
-	};
-
-	var asciifold = (function() {
-		var i, n, k, chunk;
-		var foreignletters = '';
-		var lookup = {};
-		for (k in DIACRITICS) {
-			if (DIACRITICS.hasOwnProperty(k)) {
-				chunk = DIACRITICS[k].substring(2, DIACRITICS[k].length - 1);
-				foreignletters += chunk;
-				for (i = 0, n = chunk.length; i < n; i++) {
-					lookup[chunk.charAt(i)] = k;
-				}
-			}
-		}
-		var regexp = new RegExp('[' +  foreignletters + ']', 'g');
-		return function(str) {
-			return str.replace(regexp, function(foreignletter) {
-				return lookup[foreignletter];
-			}).toLowerCase();
-		};
-	})();
-
-
-	// export
-	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-	return Sifter;
-}));
-
-
-
-/**
- * microplugin.js
- * Copyright (c) 2013 Brian Reavis & contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
- * file except in compliance with the License. You may obtain a copy of the License at:
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under
- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
- * ANY KIND, either express or implied. See the License for the specific language
- * governing permissions and limitations under the License.
- *
- * @author Brian Reavis <brian@thirdroute.com>
- */
-
-(function(root, factory) {
-	if (typeof define === 'function' && define.amd) {
-		define('microplugin', factory);
-	} else if (typeof exports === 'object') {
-		module.exports = factory();
-	} else {
-		root.MicroPlugin = factory();
-	}
-}(this, function() {
-	var MicroPlugin = {};
-
-	MicroPlugin.mixin = function(Interface) {
-		Interface.plugins = {};
-
-		/**
-		 * Initializes the listed plugins (with options).
-		 * Acceptable formats:
-		 *
-		 * List (without options):
-		 *   ['a', 'b', 'c']
-		 *
-		 * List (with options):
-		 *   [{'name': 'a', options: {}}, {'name': 'b', options: {}}]
-		 *
-		 * Hash (with options):
-		 *   {'a': { ... }, 'b': { ... }, 'c': { ... }}
-		 *
-		 * @param {mixed} plugins
-		 */
-		Interface.prototype.initializePlugins = function(plugins) {
-			var i, n, key;
-			var self  = this;
-			var queue = [];
-
-			self.plugins = {
-				names     : [],
-				settings  : {},
-				requested : {},
-				loaded    : {}
-			};
-
-			if (utils.isArray(plugins)) {
-				for (i = 0, n = plugins.length; i < n; i++) {
-					if (typeof plugins[i] === 'string') {
-						queue.push(plugins[i]);
-					} else {
-						self.plugins.settings[plugins[i].name] = plugins[i].options;
-						queue.push(plugins[i].name);
-					}
-				}
-			} else if (plugins) {
-				for (key in plugins) {
-					if (plugins.hasOwnProperty(key)) {
-						self.plugins.settings[key] = plugins[key];
-						queue.push(key);
-					}
-				}
-			}
-
-			while (queue.length) {
-				self.require(queue.shift());
-			}
-		};
-
-		Interface.prototype.loadPlugin = function(name) {
-			var self    = this;
-			var plugins = self.plugins;
-			var plugin  = Interface.plugins[name];
-
-			if (!Interface.plugins.hasOwnProperty(name)) {
-				throw new Error('Unable to find "' +  name + '" plugin');
-			}
-
-			plugins.requested[name] = true;
-			plugins.loaded[name] = plugin.fn.apply(self, [self.plugins.settings[name] || {}]);
-			plugins.names.push(name);
-		};
-
-		/**
-		 * Initializes a plugin.
-		 *
-		 * @param {string} name
-		 */
-		Interface.prototype.require = function(name) {
-			var self = this;
-			var plugins = self.plugins;
-
-			if (!self.plugins.loaded.hasOwnProperty(name)) {
-				if (plugins.requested[name]) {
-					throw new Error('Plugin has circular dependency ("' + name + '")');
-				}
-				self.loadPlugin(name);
-			}
-
-			return plugins.loaded[name];
-		};
-
-		/**
-		 * Registers a plugin.
-		 *
-		 * @param {string} name
-		 * @param {function} fn
-		 */
-		Interface.define = function(name, fn) {
-			Interface.plugins[name] = {
-				'name' : name,
-				'fn'   : fn
-			};
-		};
-	};
-
-	var utils = {
-		isArray: Array.isArray || function(vArg) {
-			return Object.prototype.toString.call(vArg) === '[object Array]';
-		}
-	};
-
-	return MicroPlugin;
-}));
-
-/**
- * selectize.js (v0.12.1)
- * Copyright (c) 2013–2015 Brian Reavis & contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
- * file except in compliance with the License. You may obtain a copy of the License at:
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under
- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
- * ANY KIND, either express or implied. See the License for the specific language
- * governing permissions and limitations under the License.
- *
- * @author Brian Reavis <brian@thirdroute.com>
- */
-
-/*jshint curly:false */
-/*jshint browser:true */
-
-(function(root, factory) {
-	if (typeof define === 'function' && define.amd) {
-		define('selectize', ['jquery','sifter','microplugin'], factory);
-	} else if (typeof exports === 'object') {
-		module.exports = factory(require('jquery'), require('sifter'), require('microplugin'));
-	} else {
-		root.Selectize = factory(root.jQuery, root.Sifter, root.MicroPlugin);
-	}
-}(this, function($, Sifter, MicroPlugin) {
-	'use strict';
-
-	var highlight = function($element, pattern) {
-		if (typeof pattern === 'string' && !pattern.length) return;
-		var regex = (typeof pattern === 'string') ? new RegExp(pattern, 'i') : pattern;
-	
-		var highlight = function(node) {
-			var skip = 0;
-			if (node.nodeType === 3) {
-				var pos = node.data.search(regex);
-				if (pos >= 0 && node.data.length > 0) {
-					var match = node.data.match(regex);
-					var spannode = document.createElement('span');
-					spannode.className = 'highlight';
-					var middlebit = node.splitText(pos);
-					var endbit = middlebit.splitText(match[0].length);
-					var middleclone = middlebit.cloneNode(true);
-					spannode.appendChild(middleclone);
-					middlebit.parentNode.replaceChild(spannode, middlebit);
-					skip = 1;
-				}
-			} else if (node.nodeType === 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) {
-				for (var i = 0; i < node.childNodes.length; ++i) {
-					i += highlight(node.childNodes[i]);
-				}
-			}
-			return skip;
-		};
-	
-		return $element.each(function() {
-			highlight(this);
-		});
-	};
-	
-	var MicroEvent = function() {};
-	MicroEvent.prototype = {
-		on: function(event, fct){
-			this._events = this._events || {};
-			this._events[event] = this._events[event] || [];
-			this._events[event].push(fct);
-		},
-		off: function(event, fct){
-			var n = arguments.length;
-			if (n === 0) return delete this._events;
-			if (n === 1) return delete this._events[event];
-	
-			this._events = this._events || {};
-			if (event in this._events === false) return;
-			this._events[event].splice(this._events[event].indexOf(fct), 1);
-		},
-		trigger: function(event /* , args... */){
-			this._events = this._events || {};
-			if (event in this._events === false) return;
-			for (var i = 0; i < this._events[event].length; i++){
-				this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1));
-			}
-		}
-	};
-	
-	/**
-	 * Mixin will delegate all MicroEvent.js function in the destination object.
-	 *
-	 * - MicroEvent.mixin(Foobar) will make Foobar able to use MicroEvent
-	 *
-	 * @param {object} the object which will support MicroEvent
-	 */
-	MicroEvent.mixin = function(destObject){
-		var props = ['on', 'off', 'trigger'];
-		for (var i = 0; i < props.length; i++){
-			destObject.prototype[props[i]] = MicroEvent.prototype[props[i]];
-		}
-	};
-	
-	var IS_MAC        = /Mac/.test(navigator.userAgent);
-	
-	var KEY_A         = 65;
-	var KEY_COMMA     = 188;
-	var KEY_RETURN    = 13;
-	var KEY_ESC       = 27;
-	var KEY_LEFT      = 37;
-	var KEY_UP        = 38;
-	var KEY_P         = 80;
-	var KEY_RIGHT     = 39;
-	var KEY_DOWN      = 40;
-	var KEY_N         = 78;
-	var KEY_BACKSPACE = 8;
-	var KEY_DELETE    = 46;
-	var KEY_SHIFT     = 16;
-	var KEY_CMD       = IS_MAC ? 91 : 17;
-	var KEY_CTRL      = IS_MAC ? 18 : 17;
-	var KEY_TAB       = 9;
-	
-	var TAG_SELECT    = 1;
-	var TAG_INPUT     = 2;
-	
-	// for now, android support in general is too spotty to support validity
-	var SUPPORTS_VALIDITY_API = !/android/i.test(window.navigator.userAgent) && !!document.createElement('form').validity;
-	
-	var isset = function(object) {
-		return typeof object !== 'undefined';
-	};
-	
-	/**
-	 * Converts a scalar to its best string representation
-	 * for hash keys and HTML attribute values.
-	 *
-	 * Transformations:
-	 *   'str'     -> 'str'
-	 *   null      -> ''
-	 *   undefined -> ''
-	 *   true      -> '1'
-	 *   false     -> '0'
-	 *   0         -> '0'
-	 *   1         -> '1'
-	 *
-	 * @param {string} value
-	 * @returns {string|null}
-	 */
-	var hash_key = function(value) {
-		if (typeof value === 'undefined' || value === null) return null;
-		if (typeof value === 'boolean') return value ? '1' : '0';
-		return value + '';
-	};
-	
-	/**
-	 * Escapes a string for use within HTML.
-	 *
-	 * @param {string} str
-	 * @returns {string}
-	 */
-	var escape_html = function(str) {
-		return (str + '')
-			.replace(/&/g, '&amp;')
-			.replace(/</g, '&lt;')
-			.replace(/>/g, '&gt;')
-			.replace(/"/g, '&quot;');
-	};
-	
-	/**
-	 * Escapes "$" characters in replacement strings.
-	 *
-	 * @param {string} str
-	 * @returns {string}
-	 */
-	var escape_replace = function(str) {
-		return (str + '').replace(/\$/g, '$$$$');
-	};
-	
-	var hook = {};
-	
-	/**
-	 * Wraps `method` on `self` so that `fn`
-	 * is invoked before the original method.
-	 *
-	 * @param {object} self
-	 * @param {string} method
-	 * @param {function} fn
-	 */
-	hook.before = function(self, method, fn) {
-		var original = self[method];
-		self[method] = function() {
-			fn.apply(self, arguments);
-			return original.apply(self, arguments);
-		};
-	};
-	
-	/**
-	 * Wraps `method` on `self` so that `fn`
-	 * is invoked after the original method.
-	 *
-	 * @param {object} self
-	 * @param {string} method
-	 * @param {function} fn
-	 */
-	hook.after = function(self, method, fn) {
-		var original = self[method];
-		self[method] = function() {
-			var result = original.apply(self, arguments);
-			fn.apply(self, arguments);
-			return result;
-		};
-	};
-	
-	/**
-	 * Wraps `fn` so that it can only be invoked once.
-	 *
-	 * @param {function} fn
-	 * @returns {function}
-	 */
-	var once = function(fn) {
-		var called = false;
-		return function() {
-			if (called) return;
-			called = true;
-			fn.apply(this, arguments);
-		};
-	};
-	
-	/**
-	 * Wraps `fn` so that it can only be called once
-	 * every `delay` milliseconds (invoked on the falling edge).
-	 *
-	 * @param {function} fn
-	 * @param {int} delay
-	 * @returns {function}
-	 */
-	var debounce = function(fn, delay) {
-		var timeout;
-		return function() {
-			var self = this;
-			var args = arguments;
-			window.clearTimeout(timeout);
-			timeout = window.setTimeout(function() {
-				fn.apply(self, args);
-			}, delay);
-		};
-	};
-	
-	/**
-	 * Debounce all fired events types listed in `types`
-	 * while executing the provided `fn`.
-	 *
-	 * @param {object} self
-	 * @param {array} types
-	 * @param {function} fn
-	 */
-	var debounce_events = function(self, types, fn) {
-		var type;
-		var trigger = self.trigger;
-		var event_args = {};
-	
-		// override trigger method
-		self.trigger = function() {
-			var type = arguments[0];
-			if (types.indexOf(type) !== -1) {
-				event_args[type] = arguments;
-			} else {
-				return trigger.apply(self, arguments);
-			}
-		};
-	
-		// invoke provided function
-		fn.apply(self, []);
-		self.trigger = trigger;
-	
-		// trigger queued events
-		for (type in event_args) {
-			if (event_args.hasOwnProperty(type)) {
-				trigger.apply(self, event_args[type]);
-			}
-		}
-	};
-	
-	/**
-	 * A workaround for http://bugs.jquery.com/ticket/6696
-	 *
-	 * @param {object} $parent - Parent element to listen on.
-	 * @param {string} event - Event name.
-	 * @param {string} selector - Descendant selector to filter by.
-	 * @param {function} fn - Event handler.
-	 */
-	var watchChildEvent = function($parent, event, selector, fn) {
-		$parent.on(event, selector, function(e) {
-			var child = e.target;
-			while (child && child.parentNode !== $parent[0]) {
-				child = child.parentNode;
-			}
-			e.currentTarget = child;
-			return fn.apply(this, [e]);
-		});
-	};
-	
-	/**
-	 * Determines the current selection within a text input control.
-	 * Returns an object containing:
-	 *   - start
-	 *   - length
-	 *
-	 * @param {object} input
-	 * @returns {object}
-	 */
-	var getSelection = function(input) {
-		var result = {};
-		if ('selectionStart' in input) {
-			result.start = input.selectionStart;
-			result.length = input.selectionEnd - result.start;
-		} else if (document.selection) {
-			input.focus();
-			var sel = document.selection.createRange();
-			var selLen = document.selection.createRange().text.length;
-			sel.moveStart('character', -input.value.length);
-			result.start = sel.text.length - selLen;
-			result.length = selLen;
-		}
-		return result;
-	};
-	
-	/**
-	 * Copies CSS properties from one element to another.
-	 *
-	 * @param {object} $from
-	 * @param {object} $to
-	 * @param {array} properties
-	 */
-	var transferStyles = function($from, $to, properties) {
-		var i, n, styles = {};
-		if (properties) {
-			for (i = 0, n = properties.length; i < n; i++) {
-				styles[properties[i]] = $from.css(properties[i]);
-			}
-		} else {
-			styles = $from.css();
-		}
-		$to.css(styles);
-	};
-	
-	/**
-	 * Measures the width of a string within a
-	 * parent element (in pixels).
-	 *
-	 * @param {string} str
-	 * @param {object} $parent
-	 * @returns {int}
-	 */
-	var measureString = function(str, $parent) {
-		if (!str) {
-			return 0;
-		}
-	
-		var $test = $('<test>').css({
-			position: 'absolute',
-			top: -99999,
-			left: -99999,
-			width: 'auto',
-			padding: 0,
-			whiteSpace: 'pre'
-		}).text(str).appendTo('body');
-	
-		transferStyles($parent, $test, [
-			'letterSpacing',
-			'fontSize',
-			'fontFamily',
-			'fontWeight',
-			'textTransform'
-		]);
-	
-		var width = $test.width();
-		$test.remove();
-	
-		return width;
-	};
-	
-	/**
-	 * Sets up an input to grow horizontally as the user
-	 * types. If the value is changed manually, you can
-	 * trigger the "update" handler to resize:
-	 *
-	 * $input.trigger('update');
-	 *
-	 * @param {object} $input
-	 */
-	var autoGrow = function($input) {
-		var currentWidth = null;
-	
-		var update = function(e, options) {
-			var value, keyCode, printable, placeholder, width;
-			var shift, character, selection;
-			e = e || window.event || {};
-			options = options || {};
-	
-			if (e.metaKey || e.altKey) return;
-			if (!options.force && $input.data('grow') === false) return;
-	
-			value = $input.val();
-			if (e.type && e.type.toLowerCase() === 'keydown') {
-				keyCode = e.keyCode;
-				printable = (
-					(keyCode >= 97 && keyCode <= 122) || // a-z
-					(keyCode >= 65 && keyCode <= 90)  || // A-Z
-					(keyCode >= 48 && keyCode <= 57)  || // 0-9
-					keyCode === 32 // space
-				);
-	
-				if (keyCode === KEY_DELETE || keyCode === KEY_BACKSPACE) {
-					selection = getSelection($input[0]);
-					if (selection.length) {
-						value = value.substring(0, selection.start) + value.substring(selection.start + selection.length);
-					} else if (keyCode === KEY_BACKSPACE && selection.start) {
-						value = value.substring(0, selection.start - 1) + value.substring(selection.start + 1);
-					} else if (keyCode === KEY_DELETE && typeof selection.start !== 'undefined') {
-						value = value.substring(0, selection.start) + value.substring(selection.start + 1);
-					}
-				} else if (printable) {
-					shift = e.shiftKey;
-					character = String.fromCharCode(e.keyCode);
-					if (shift) character = character.toUpperCase();
-					else character = character.toLowerCase();
-					value += character;
-				}
-			}
-	
-			placeholder = $input.attr('placeholder');
-			if (!value && placeholder) {
-				value = placeholder;
-			}
-	
-			width = measureString(value, $input) + 4;
-			if (width !== currentWidth) {
-				currentWidth = width;
-				$input.width(width);
-				$input.triggerHandler('resize');
-			}
-		};
-	
-		$input.on('keydown keyup update blur', update);
-		update();
-	};
-	
-	var Selectize = function($input, settings) {
-		var key, i, n, dir, input, self = this;
-		input = $input[0];
-		input.selectize = self;
-	
-		// detect rtl environment
-		var computedStyle = window.getComputedStyle && window.getComputedStyle(input, null);
-		dir = computedStyle ? computedStyle.getPropertyValue('direction') : input.currentStyle && input.currentStyle.direction;
-		dir = dir || $input.parents('[dir]:first').attr('dir') || '';
-	
-		// setup default state
-		$.extend(self, {
-			order            : 0,
-			settings         : settings,
-			$input           : $input,
-			tabIndex         : $input.attr('tabindex') || '',
-			tagType          : input.tagName.toLowerCase() === 'select' ? TAG_SELECT : TAG_INPUT,
-			rtl              : /rtl/i.test(dir),
-	
-			eventNS          : '.selectize' + (++Selectize.count),
-			highlightedValue : null,
-			isOpen           : false,
-			isDisabled       : false,
-			isRequired       : $input.is('[required]'),
-			isInvalid        : false,
-			isLocked         : false,
-			isFocused        : false,
-			isInputHidden    : false,
-			isSetup          : false,
-			isShiftDown      : false,
-			isCmdDown        : false,
-			isCtrlDown       : false,
-			ignoreFocus      : false,
-			ignoreBlur       : false,
-			ignoreHover      : false,
-			hasOptions       : false,
-			currentResults   : null,
-			lastValue        : '',
-			caretPos         : 0,
-			loading          : 0,
-			loadedSearches   : {},
-	
-			$activeOption    : null,
-			$activeItems     : [],
-	
-			optgroups        : {},
-			options          : {},
-			userOptions      : {},
-			items            : [],
-			renderCache      : {},
-			onSearchChange   : settings.loadThrottle === null ? self.onSearchChange : debounce(self.onSearchChange, settings.loadThrottle)
-		});
-	
-		// search system
-		self.sifter = new Sifter(this.options, {diacritics: settings.diacritics});
-	
-		// build options table
-		if (self.settings.options) {
-			for (i = 0, n = self.settings.options.length; i < n; i++) {
-				self.registerOption(self.settings.options[i]);
-			}
-			delete self.settings.options;
-		}
-	
-		// build optgroup table
-		if (self.settings.optgroups) {
-			for (i = 0, n = self.settings.optgroups.length; i < n; i++) {
-				self.registerOptionGroup(self.settings.optgroups[i]);
-			}
-			delete self.settings.optgroups;
-		}
-	
-		// option-dependent defaults
-		self.settings.mode = self.settings.mode || (self.settings.maxItems === 1 ? 'single' : 'multi');
-		if (typeof self.settings.hideSelected !== 'boolean') {
-			self.settings.hideSelected = self.settings.mode === 'multi';
-		}
-	
-		self.initializePlugins(self.settings.plugins);
-		self.setupCallbacks();
-		self.setupTemplates();
-		self.setup();
-	};
-	
-	// mixins
-	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-	
-	MicroEvent.mixin(Selectize);
-	MicroPlugin.mixin(Selectize);
-	
-	// methods
-	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-	
-	$.extend(Selectize.prototype, {
-	
-		/**
-		 * Creates all elements and sets up event bindings.
-		 */
-		setup: function() {
-			var self      = this;
-			var settings  = self.settings;
-			var eventNS   = self.eventNS;
-			var $window   = $(window);
-			var $document = $(document);
-			var $input    = self.$input;
-	
-			var $wrapper;
-			var $control;
-			var $control_input;
-			var $dropdown;
-			var $dropdown_content;
-			var $dropdown_parent;
-			var inputMode;
-			var timeout_blur;
-			var timeout_focus;
-			var classes;
-			var classes_plugins;
-	
-			inputMode         = self.settings.mode;
-			classes           = $input.attr('class') || '';
-	
-			$wrapper          = $('<div>').addClass(settings.wrapperClass).addClass(classes).addClass(inputMode);
-			$control          = $('<div>').addClass(settings.inputClass).addClass('items').appendTo($wrapper);
-			$control_input    = $('<input type="text" autocomplete="off" />').appendTo($control).attr('tabindex', $input.is(':disabled') ? '-1' : self.tabIndex);
-			$dropdown_parent  = $(settings.dropdownParent || $wrapper);
-			$dropdown         = $('<div>').addClass(settings.dropdownClass).addClass(inputMode).hide().appendTo($dropdown_parent);
-			$dropdown_content = $('<div>').addClass(settings.dropdownContentClass).appendTo($dropdown);
-	
-			if(self.settings.copyClassesToDropdown) {
-				$dropdown.addClass(classes);
-			}
-	
-			$wrapper.css({
-				width: $input[0].style.width
-			});
-	
-			if (self.plugins.names.length) {
-				classes_plugins = 'plugin-' + self.plugins.names.join(' plugin-');
-				$wrapper.addClass(classes_plugins);
-				$dropdown.addClass(classes_plugins);
-			}
-	
-			if ((settings.maxItems === null || settings.maxItems > 1) && self.tagType === TAG_SELECT) {
-				$input.attr('multiple', 'multiple');
-			}
-	
-			if (self.settings.placeholder) {
-				$control_input.attr('placeholder', settings.placeholder);
-			}
-	
-			// if splitOn was not passed in, construct it from the delimiter to allow pasting universally
-			if (!self.settings.splitOn && self.settings.delimiter) {
-				var delimiterEscaped = self.settings.delimiter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
-				self.settings.splitOn = new RegExp('\\s*' + delimiterEscaped + '+\\s*');
-			}
-	
-			if ($input.attr('autocorrect')) {
-				$control_input.attr('autocorrect', $input.attr('autocorrect'));
-			}
-	
-			if ($input.attr('autocapitalize')) {
-				$control_input.attr('autocapitalize', $input.attr('autocapitalize'));
-			}
-	
-			self.$wrapper          = $wrapper;
-			self.$control          = $control;
-			self.$control_input    = $control_input;
-			self.$dropdown         = $dropdown;
-			self.$dropdown_content = $dropdown_content;
-	
-			$dropdown.on('mouseenter', '[data-selectable]', function() { return self.onOptionHover.apply(self, arguments); });
-			$dropdown.on('mousedown click', '[data-selectable]', function() { return self.onOptionSelect.apply(self, arguments); });
-			watchChildEvent($control, 'mousedown', '*:not(input)', function() { return self.onItemSelect.apply(self, arguments); });
-			autoGrow($control_input);
-	
-			$control.on({
-				mousedown : function() { return self.onMouseDown.apply(self, arguments); },
-				click     : function() { return self.onClick.apply(self, arguments); }
-			});
-	
-			$control_input.on({
-				mousedown : function(e) { e.stopPropagation(); },
-				keydown   : function() { return self.onKeyDown.apply(self, arguments); },
-				keyup     : function() { return self.onKeyUp.apply(self, arguments); },
-				keypress  : function() { return self.onKeyPress.apply(self, arguments); },
-				resize    : function() { self.positionDropdown.apply(self, []); },
-				blur      : function() { return self.onBlur.apply(self, arguments); },
-				focus     : function() { self.ignoreBlur = false; return self.onFocus.apply(self, arguments); },
-				paste     : function() { return self.onPaste.apply(self, arguments); }
-			});
-	
-			$document.on('keydown' + eventNS, function(e) {
-				self.isCmdDown = e[IS_MAC ? 'metaKey' : 'ctrlKey'];
-				self.isCtrlDown = e[IS_MAC ? 'altKey' : 'ctrlKey'];
-				self.isShiftDown = e.shiftKey;
-			});
-	
-			$document.on('keyup' + eventNS, function(e) {
-				if (e.keyCode === KEY_CTRL) self.isCtrlDown = false;
-				if (e.keyCode === KEY_SHIFT) self.isShiftDown = false;
-				if (e.keyCode === KEY_CMD) self.isCmdDown = false;
-			});
-	
-			$document.on('mousedown' + eventNS, function(e) {
-				if (self.isFocused) {
-					// prevent events on the dropdown scrollbar from causing the control to blur
-					if (e.target === self.$dropdown[0] || e.target.parentNode === self.$dropdown[0]) {
-						return false;
-					}
-					// blur on click outside
-					if (!self.$control.has(e.target).length && e.target !== self.$control[0]) {
-						self.blur(e.target);
-					}
-				}
-			});
-	
-			$window.on(['scroll' + eventNS, 'resize' + eventNS].join(' '), function() {
-				if (self.isOpen) {
-					self.positionDropdown.apply(self, arguments);
-				}
-			});
-			$window.on('mousemove' + eventNS, function() {
-				self.ignoreHover = false;
-			});
-	
-			// store original children and tab index so that they can be
-			// restored when the destroy() method is called.
-			this.revertSettings = {
-				$children : $input.children().detach(),
-				tabindex  : $input.attr('tabindex')
-			};
-	
-			$input.attr('tabindex', -1).hide().after(self.$wrapper);
-	
-			if ($.isArray(settings.items)) {
-				self.setValue(settings.items);
-				delete settings.items;
-			}
-	
-			// feature detect for the validation API
-			if (SUPPORTS_VALIDITY_API) {
-				$input.on('invalid' + eventNS, function(e) {
-					e.preventDefault();
-					self.isInvalid = true;
-					self.refreshState();
-				});
-			}
-	
-			self.updateOriginalInput();
-			self.refreshItems();
-			self.refreshState();
-			self.updatePlaceholder();
-			self.isSetup = true;
-	
-			if ($input.is(':disabled')) {
-				self.disable();
-			}
-	
-			self.on('change', this.onChange);
-	
-			$input.data('selectize', self);
-			$input.addClass('selectized');
-			self.trigger('initialize');
-	
-			// preload options
-			if (settings.preload === true) {
-				self.onSearchChange('');
-			}
-	
-		},
-	
-		/**
-		 * Sets up default rendering functions.
-		 */
-		setupTemplates: function() {
-			var self = this;
-			var field_label = self.settings.labelField;
-			var field_optgroup = self.settings.optgroupLabelField;
-	
-			var templates = {
-				'optgroup': function(data) {
-					return '<div class="optgroup">' + data.html + '</div>';
-				},
-				'optgroup_header': function(data, escape) {
-					return '<div class="optgroup-header">' + escape(data[field_optgroup]) + '</div>';
-				},
-				'option': function(data, escape) {
-					return '<div class="option">' + escape(data[field_label]) + '</div>';
-				},
-				'item': function(data, escape) {
-					return '<div class="item">' + escape(data[field_label]) + '</div>';
-				},
-				'option_create': function(data, escape) {
-					return '<div class="create">Add <strong>' + escape(data.input) + '</strong>&hellip;</div>';
-				}
-			};
-	
-			self.settings.render = $.extend({}, templates, self.settings.render);
-		},
-	
-		/**
-		 * Maps fired events to callbacks provided
-		 * in the settings used when creating the control.
-		 */
-		setupCallbacks: function() {
-			var key, fn, callbacks = {
-				'initialize'      : 'onInitialize',
-				'change'          : 'onChange',
-				'item_add'        : 'onItemAdd',
-				'item_remove'     : 'onItemRemove',
-				'clear'           : 'onClear',
-				'option_add'      : 'onOptionAdd',
-				'option_remove'   : 'onOptionRemove',
-				'option_clear'    : 'onOptionClear',
-				'optgroup_add'    : 'onOptionGroupAdd',
-				'optgroup_remove' : 'onOptionGroupRemove',
-				'optgroup_clear'  : 'onOptionGroupClear',
-				'dropdown_open'   : 'onDropdownOpen',
-				'dropdown_close'  : 'onDropdownClose',
-				'type'            : 'onType',
-				'load'            : 'onLoad',
-				'focus'           : 'onFocus',
-				'blur'            : 'onBlur'
-			};
-	
-			for (key in callbacks) {
-				if (callbacks.hasOwnProperty(key)) {
-					fn = this.settings[callbacks[key]];
-					if (fn) this.on(key, fn);
-				}
-			}
-		},
-	
-		/**
-		 * Triggered when the main control element
-		 * has a click event.
-		 *
-		 * @param {object} e
-		 * @return {boolean}
-		 */
-		onClick: function(e) {
-			var self = this;
-	
-			// necessary for mobile webkit devices (manual focus triggering
-			// is ignored unless invoked within a click event)
-			if (!self.isFocused) {
-				self.focus();
-				e.preventDefault();
-			}
-		},
-	
-		/**
-		 * Triggered when the main control element
-		 * has a mouse down event.
-		 *
-		 * @param {object} e
-		 * @return {boolean}
-		 */
-		onMouseDown: function(e) {
-			var self = this;
-			var defaultPrevented = e.isDefaultPrevented();
-			var $target = $(e.target);
-	
-			if (self.isFocused) {
-				// retain focus by preventing native handling. if the
-				// event target is the input it should not be modified.
-				// otherwise, text selection within the input won't work.
-				if (e.target !== self.$control_input[0]) {
-					if (self.settings.mode === 'single') {
-						// toggle dropdown
-						self.isOpen ? self.close() : self.open();
-					} else if (!defaultPrevented) {
-						self.setActiveItem(null);
-					}
-					return false;
-				}
-			} else {
-				// give control focus
-				if (!defaultPrevented) {
-					window.setTimeout(function() {
-						self.focus();
-					}, 0);
-				}
-			}
-		},
-	
-		/**
-		 * Triggered when the value of the control has been changed.
-		 * This should propagate the event to the original DOM
-		 * input / select element.
-		 */
-		onChange: function() {
-			this.$input.trigger('change');
-		},
-	
-		/**
-		 * Triggered on <input> paste.
-		 *
-		 * @param {object} e
-		 * @returns {boolean}
-		 */
-		onPaste: function(e) {
-			var self = this;
-			if (self.isFull() || self.isInputHidden || self.isLocked) {
-				e.preventDefault();
-			} else {
-				// If a regex or string is included, this will split the pasted
-				// input and create Items for each separate value
-				if (self.settings.splitOn) {
-					setTimeout(function() {
-						var splitInput = $.trim(self.$control_input.val() || '').split(self.settings.splitOn);
-						for (var i = 0, n = splitInput.length; i < n; i++) {
-							self.createItem(splitInput[i]);
-						}
-					}, 0);
-				}
-			}
-		},
-	
-		/**
-		 * Triggered on <input> keypress.
-		 *
-		 * @param {object} e
-		 * @returns {boolean}
-		 */
-		onKeyPress: function(e) {
-			if (this.isLocked) return e && e.preventDefault();
-			var character = String.fromCharCode(e.keyCode || e.which);
-			if (this.settings.create && this.settings.mode === 'multi' && character === this.settings.delimiter) {
-				this.createItem();
-				e.preventDefault();
-				return false;
-			}
-		},
-	
-		/**
-		 * Triggered on <input> keydown.
-		 *
-		 * @param {object} e
-		 * @returns {boolean}
-		 */
-		onKeyDown: function(e) {
-			var isInput = e.target === this.$control_input[0];
-			var self = this;
-	
-			if (self.isLocked) {
-				if (e.keyCode !== KEY_TAB) {
-					e.preventDefault();
-				}
-				return;
-			}
-	
-			switch (e.keyCode) {
-				case KEY_A:
-					if (self.isCmdDown) {
-						self.selectAll();
-						return;
-					}
-					break;
-				case KEY_ESC:
-					if (self.isOpen) {
-						e.preventDefault();
-						e.stopPropagation();
-						self.close();
-					}
-					return;
-				case KEY_N:
-					if (!e.ctrlKey || e.altKey) break;
-				case KEY_DOWN:
-					if (!self.isOpen && self.hasOptions) {
-						self.open();
-					} else if (self.$activeOption) {
-						self.ignoreHover = true;
-						var $next = self.getAdjacentOption(self.$activeOption, 1);
-						if ($next.length) self.setActiveOption($next, true, true);
-					}
-					e.preventDefault();
-					return;
-				case KEY_P:
-					if (!e.ctrlKey || e.altKey) break;
-				case KEY_UP:
-					if (self.$activeOption) {
-						self.ignoreHover = true;
-						var $prev = self.getAdjacentOption(self.$activeOption, -1);
-						if ($prev.length) self.setActiveOption($prev, true, true);
-					}
-					e.preventDefault();
-					return;
-				case KEY_RETURN:
-					if (self.isOpen && self.$activeOption) {
-						self.onOptionSelect({currentTarget: self.$activeOption});
-						e.preventDefault();
-					}
-					return;
-				case KEY_LEFT:
-					self.advanceSelection(-1, e);
-					return;
-				case KEY_RIGHT:
-					self.advanceSelection(1, e);
-					return;
-				case KEY_TAB:
-					if (self.settings.selectOnTab && self.isOpen && self.$activeOption) {
-						self.onOptionSelect({currentTarget: self.$activeOption});
-	
-						// Default behaviour is to jump to the next field, we only want this
-						// if the current field doesn't accept any more entries
-						if (!self.isFull()) {
-							e.preventDefault();
-						}
-					}
-					if (self.settings.create && self.createItem()) {
-						e.preventDefault();
-					}
-					return;
-				case KEY_BACKSPACE:
-				case KEY_DELETE:
-					self.deleteSelection(e);
-					return;
-			}
-	
-			if ((self.isFull() || self.isInputHidden) && !(IS_MAC ? e.metaKey : e.ctrlKey)) {
-				e.preventDefault();
-				return;
-			}
-		},
-	
-		/**
-		 * Triggered on <input> keyup.
-		 *
-		 * @param {object} e
-		 * @returns {boolean}
-		 */
-		onKeyUp: function(e) {
-			var self = this;
-	
-			if (self.isLocked) return e && e.preventDefault();
-			var value = self.$control_input.val() || '';
-			if (self.lastValue !== value) {
-				self.lastValue = value;
-				self.onSearchChange(value);
-				self.refreshOptions();
-				self.trigger('type', value);
-			}
-		},
-	
-		/**
-		 * Invokes the user-provide option provider / loader.
-		 *
-		 * Note: this function is debounced in the Selectize
-		 * constructor (by `settings.loadDelay` milliseconds)
-		 *
-		 * @param {string} value
-		 */
-		onSearchChange: function(value) {
-			var self = this;
-			var fn = self.settings.load;
-			if (!fn) return;
-			if (self.loadedSearches.hasOwnProperty(value)) return;
-			self.loadedSearches[value] = true;
-			self.load(function(callback) {
-				fn.apply(self, [value, callback]);
-			});
-		},
-	
-		/**
-		 * Triggered on <input> focus.
-		 *
-		 * @param {object} e (optional)
-		 * @returns {boolean}
-		 */
-		onFocus: function(e) {
-			var self = this;
-			var wasFocused = self.isFocused;
-	
-			if (self.isDisabled) {
-				self.blur();
-				e && e.preventDefault();
-				return false;
-			}
-	
-			if (self.ignoreFocus) return;
-			self.isFocused = true;
-			if (self.settings.preload === 'focus') self.onSearchChange('');
-	
-			if (!wasFocused) self.trigger('focus');
-	
-			if (!self.$activeItems.length) {
-				self.showInput();
-				self.setActiveItem(null);
-				self.refreshOptions(!!self.settings.openOnFocus);
-			}
-	
-			self.refreshState();
-		},
-	
-		/**
-		 * Triggered on <input> blur.
-		 *
-		 * @param {object} e
-		 * @param {Element} dest
-		 */
-		onBlur: function(e, dest) {
-			var self = this;
-			if (!self.isFocused) return;
-			self.isFocused = false;
-	
-			if (self.ignoreFocus) {
-				return;
-			} else if (!self.ignoreBlur && document.activeElement === self.$dropdown_content[0]) {
-				// necessary to prevent IE closing the dropdown when the scrollbar is clicked
-				self.ignoreBlur = true;
-				self.onFocus(e);
-				return;
-			}
-	
-			var deactivate = function() {
-				self.close();
-				self.setTextboxValue('');
-				self.setActiveItem(null);
-				self.setActiveOption(null);
-				self.setCaret(self.items.length);
-				self.refreshState();
-	
-				// IE11 bug: element still marked as active
-				(dest || document.body).focus();
-	
-				self.ignoreFocus = false;
-				self.trigger('blur');
-			};
-	
-			self.ignoreFocus = true;
-			if (self.settings.create && self.settings.createOnBlur) {
-				self.createItem(null, false, deactivate);
-			} else {
-				deactivate();
-			}
-		},
-	
-		/**
-		 * Triggered when the user rolls over
-		 * an option in the autocomplete dropdown menu.
-		 *
-		 * @param {object} e
-		 * @returns {boolean}
-		 */
-		onOptionHover: function(e) {
-			if (this.ignoreHover) return;
-			this.setActiveOption(e.currentTarget, false);
-		},
-	
-		/**
-		 * Triggered when the user clicks on an option
-		 * in the autocomplete dropdown menu.
-		 *
-		 * @param {object} e
-		 * @returns {boolean}
-		 */
-		onOptionSelect: function(e) {
-			var value, $target, $option, self = this;
-	
-			if (e.preventDefault) {
-				e.preventDefault();
-				e.stopPropagation();
-			}
-	
-			$target = $(e.currentTarget);
-			if ($target.hasClass('create')) {
-				self.createItem(null, function() {
-					if (self.settings.closeAfterSelect) {
-						self.close();
-					}
-				});
-			} else {
-				value = $target.attr('data-value');
-				if (typeof value !== 'undefined') {
-					self.lastQuery = null;
-					self.setTextboxValue('');
-					self.addItem(value);
-					if (self.settings.closeAfterSelect) {
-						self.close();
-					} else if (!self.settings.hideSelected && e.type && /mouse/.test(e.type)) {
-						self.setActiveOption(self.getOption(value));
-					}
-				}
-			}
-		},
-	
-		/**
-		 * Triggered when the user clicks on an item
-		 * that has been selected.
-		 *
-		 * @param {object} e
-		 * @returns {boolean}
-		 */
-		onItemSelect: function(e) {
-			var self = this;
-	
-			if (self.isLocked) return;
-			if (self.settings.mode === 'multi') {
-				e.preventDefault();
-				self.setActiveItem(e.currentTarget, e);
-			}
-		},
-	
-		/**
-		 * Invokes the provided method that provides
-		 * results to a callback---which are then added
-		 * as options to the control.
-		 *
-		 * @param {function} fn
-		 */
-		load: function(fn) {
-			var self = this;
-			var $wrapper = self.$wrapper.addClass(self.settings.loadingClass);
-	
-			self.loading++;
-			fn.apply(self, [function(results) {
-				self.loading = Math.max(self.loading - 1, 0);
-				if (results && results.length) {
-					self.addOption(results);
-					self.refreshOptions(self.isFocused && !self.isInputHidden);
-				}
-				if (!self.loading) {
-					$wrapper.removeClass(self.settings.loadingClass);
-				}
-				self.trigger('load', results);
-			}]);
-		},
-	
-		/**
-		 * Sets the input field of the control to the specified value.
-		 *
-		 * @param {string} value
-		 */
-		setTextboxValue: function(value) {
-			var $input = this.$control_input;
-			var changed = $input.val() !== value;
-			if (changed) {
-				$input.val(value).triggerHandler('update');
-				this.lastValue = value;
-			}
-		},
-	
-		/**
-		 * Returns the value of the control. If multiple items
-		 * can be selected (e.g. <select multiple>), this returns
-		 * an array. If only one item can be selected, this
-		 * returns a string.
-		 *
-		 * @returns {mixed}
-		 */
-		getValue: function() {
-			if (this.tagType === TAG_SELECT && this.$input.attr('multiple')) {
-				return this.items;
-			} else {
-				return this.items.join(this.settings.delimiter);
-			}
-		},
-	
-		/**
-		 * Resets the selected items to the given value.
-		 *
-		 * @param {mixed} value
-		 */
-		setValue: function(value, silent) {
-			var events = silent ? [] : ['change'];
-	
-			debounce_events(this, events, function() {
-				this.clear(silent);
-				this.addItems(value, silent);
-			});
-		},
-	
-		/**
-		 * Sets the selected item.
-		 *
-		 * @param {object} $item
-		 * @param {object} e (optional)
-		 */
-		setActiveItem: function($item, e) {
-			var self = this;
-			var eventName;
-			var i, idx, begin, end, item, swap;
-			var $last;
-	
-			if (self.settings.mode === 'single') return;
-			$item = $($item);
-	
-			// clear the active selection
-			if (!$item.length) {
-				$(self.$activeItems).removeClass('active');
-				self.$activeItems = [];
-				if (self.isFocused) {
-					self.showInput();
-				}
-				return;
-			}
-	
-			// modify selection
-			eventName = e && e.type.toLowerCase();
-	
-			if (eventName === 'mousedown' && self.isShiftDown && self.$activeItems.length) {
-				$last = self.$control.children('.active:last');
-				begin = Array.prototype.indexOf.apply(self.$control[0].childNodes, [$last[0]]);
-				end   = Array.prototype.indexOf.apply(self.$control[0].childNodes, [$item[0]]);
-				if (begin > end) {
-					swap  = begin;
-					begin = end;
-					end   = swap;
-				}
-				for (i = begin; i <= end; i++) {
-					item = self.$control[0].childNodes[i];
-					if (self.$activeItems.indexOf(item) === -1) {
-						$(item).addClass('active');
-						self.$activeItems.push(item);
-					}
-				}
-				e.preventDefault();
-			} else if ((eventName === 'mousedown' && self.isCtrlDown) || (eventName === 'keydown' && this.isShiftDown)) {
-				if ($item.hasClass('active')) {
-					idx = self.$activeItems.indexOf($item[0]);
-					self.$activeItems.splice(idx, 1);
-					$item.removeClass('active');
-				} else {
-					self.$activeItems.push($item.addClass('active')[0]);
-				}
-			} else {
-				$(self.$activeItems).removeClass('active');
-				self.$activeItems = [$item.addClass('active')[0]];
-			}
-	
-			// ensure control has focus
-			self.hideInput();
-			if (!this.isFocused) {
-				self.focus();
-			}
-		},
-	
-		/**
-		 * Sets the selected item in the dropdown menu
-		 * of available options.
-		 *
-		 * @param {object} $object
-		 * @param {boolean} scroll
-		 * @param {boolean} animate
-		 */
-		setActiveOption: function($option, scroll, animate) {
-			var height_menu, height_item, y;
-			var scroll_top, scroll_bottom;
-			var self = this;
-	
-			if (self.$activeOption) self.$activeOption.removeClass('active');
-			self.$activeOption = null;
-	
-			$option = $($option);
-			if (!$option.length) return;
-	
-			self.$activeOption = $option.addClass('active');
-	
-			if (scroll || !isset(scroll)) {
-	
-				height_menu   = self.$dropdown_content.height();
-				height_item   = self.$activeOption.outerHeight(true);
-				scroll        = self.$dropdown_content.scrollTop() || 0;
-				y             = self.$activeOption.offset().top - self.$dropdown_content.offset().top + scroll;
-				scroll_top    = y;
-				scroll_bottom = y - height_menu + height_item;
-	
-				if (y + height_item > height_menu + scroll) {
-					self.$dropdown_content.stop().animate({scrollTop: scroll_bottom}, animate ? self.settings.scrollDuration : 0);
-				} else if (y < scroll) {
-					self.$dropdown_content.stop().animate({scrollTop: scroll_top}, animate ? self.settings.scrollDuration : 0);
-				}
-	
-			}
-		},
-	
-		/**
-		 * Selects all items (CTRL + A).
-		 */
-		selectAll: function() {
-			var self = this;
-			if (self.settings.mode === 'single') return;
-	
-			self.$activeItems = Array.prototype.slice.apply(self.$control.children(':not(input)').addClass('active'));
-			if (self.$activeItems.length) {
-				self.hideInput();
-				self.close();
-			}
-			self.focus();
-		},
-	
-		/**
-		 * Hides the input element out of view, while
-		 * retaining its focus.
-		 */
-		hideInput: function() {
-			var self = this;
-	
-			self.setTextboxValue('');
-			self.$control_input.css({opacity: 0, position: 'absolute', left: self.rtl ? 10000 : -10000});
-			self.isInputHidden = true;
-		},
-	
-		/**
-		 * Restores input visibility.
-		 */
-		showInput: function() {
-			this.$control_input.css({opacity: 1, position: 'relative', left: 0});
-			this.isInputHidden = false;
-		},
-	
-		/**
-		 * Gives the control focus.
-		 */
-		focus: function() {
-			var self = this;
-			if (self.isDisabled) return;
-	
-			self.ignoreFocus = true;
-			self.$control_input[0].focus();
-			window.setTimeout(function() {
-				self.ignoreFocus = false;
-				self.onFocus();
-			}, 0);
-		},
-	
-		/**
-		 * Forces the control out of focus.
-		 *
-		 * @param {Element} dest
-		 */
-		blur: function(dest) {
-			this.$control_input[0].blur();
-			this.onBlur(null, dest);
-		},
-	
-		/**
-		 * Returns a function that scores an object
-		 * to show how good of a match it is to the
-		 * provided query.
-		 *
-		 * @param {string} query
-		 * @param {object} options
-		 * @return {function}
-		 */
-		getScoreFunction: function(query) {
-			return this.sifter.getScoreFunction(query, this.getSearchOptions());
-		},
-	
-		/**
-		 * Returns search options for sifter (the system
-		 * for scoring and sorting results).
-		 *
-		 * @see https://github.com/brianreavis/sifter.js
-		 * @return {object}
-		 */
-		getSearchOptions: function() {
-			var settings = this.settings;
-			var sort = settings.sortField;
-			if (typeof sort === 'string') {
-				sort = [{field: sort}];
-			}
-	
-			return {
-				fields      : settings.searchField,
-				conjunction : settings.searchConjunction,
-				sort        : sort
-			};
-		},
-	
-		/**
-		 * Searches through available options and returns
-		 * a sorted array of matches.
-		 *
-		 * Returns an object containing:
-		 *
-		 *   - query {string}
-		 *   - tokens {array}
-		 *   - total {int}
-		 *   - items {array}
-		 *
-		 * @param {string} query
-		 * @returns {object}
-		 */
-		search: function(query) {
-			var i, value, score, result, calculateScore;
-			var self     = this;
-			var settings = self.settings;
-			var options  = this.getSearchOptions();
-	
-			// validate user-provided result scoring function
-			if (settings.score) {
-				calculateScore = self.settings.score.apply(this, [query]);
-				if (typeof calculateScore !== 'function') {
-					throw new Error('Selectize "score" setting must be a function that returns a function');
-				}
-			}
-	
-			// perform search
-			if (query !== self.lastQuery) {
-				self.lastQuery = query;
-				result = self.sifter.search(query, $.extend(options, {score: calculateScore}));
-				self.currentResults = result;
-			} else {
-				result = $.extend(true, {}, self.currentResults);
-			}
-	
-			// filter out selected items
-			if (settings.hideSelected) {
-				for (i = result.items.length - 1; i >= 0; i--) {
-					if (self.items.indexOf(hash_key(result.items[i].id)) !== -1) {
-						result.items.splice(i, 1);
-					}
-				}
-			}
-	
-			return result;
-		},
-	
-		/**
-		 * Refreshes the list of available options shown
-		 * in the autocomplete dropdown menu.
-		 *
-		 * @param {boolean} triggerDropdown
-		 */
-		refreshOptions: function(triggerDropdown) {
-			var i, j, k, n, groups, groups_order, option, option_html, optgroup, optgroups, html, html_children, has_create_option;
-			var $active, $active_before, $create;
-	
-			if (typeof triggerDropdown === 'undefined') {
-				triggerDropdown = true;
-			}
-	
-			var self              = this;
-			var query             = $.trim(self.$control_input.val());
-			var results           = self.search(query);
-			var $dropdown_content = self.$dropdown_content;
-			var active_before     = self.$activeOption && hash_key(self.$activeOption.attr('data-value'));
-	
-			// build markup
-			n = results.items.length;
-			if (typeof self.settings.maxOptions === 'number') {
-				n = Math.min(n, self.settings.maxOptions);
-			}
-	
-			// render and group available options individually
-			groups = {};
-			groups_order = [];
-	
-			for (i = 0; i < n; i++) {
-				option      = self.options[results.items[i].id];
-				option_html = self.render('option', option);
-				optgroup    = option[self.settings.optgroupField] || '';
-				optgroups   = $.isArray(optgroup) ? optgroup : [optgroup];
-	
-				for (j = 0, k = optgroups && optgroups.length; j < k; j++) {
-					optgroup = optgroups[j];
-					if (!self.optgroups.hasOwnProperty(optgroup)) {
-						optgroup = '';
-					}
-					if (!groups.hasOwnProperty(optgroup)) {
-						groups[optgroup] = [];
-						groups_order.push(optgroup);
-					}
-					groups[optgroup].push(option_html);
-				}
-			}
-	
-			// sort optgroups
-			if (this.settings.lockOptgroupOrder) {
-				groups_order.sort(function(a, b) {
-					var a_order = self.optgroups[a].$order || 0;
-					var b_order = self.optgroups[b].$order || 0;
-					return a_order - b_order;
-				});
-			}
-	
-			// render optgroup headers & join groups
-			html = [];
-			for (i = 0, n = groups_order.length; i < n; i++) {
-				optgroup = groups_order[i];
-				if (self.optgroups.hasOwnProperty(optgroup) && groups[optgroup].length) {
-					// render the optgroup header and options within it,
-					// then pass it to the wrapper template
-					html_children = self.render('optgroup_header', self.optgroups[optgroup]) || '';
-					html_children += groups[optgroup].join('');
-					html.push(self.render('optgroup', $.extend({}, self.optgroups[optgroup], {
-						html: html_children
-					})));
-				} else {
-					html.push(groups[optgroup].join(''));
-				}
-			}
-	
-			$dropdown_content.html(html.join(''));
-	
-			// highlight matching terms inline
-			if (self.settings.highlight && results.query.length && results.tokens.length) {
-				for (i = 0, n = results.tokens.length; i < n; i++) {
-					highlight($dropdown_content, results.tokens[i].regex);
-				}
-			}
-	
-			// add "selected" class to selected options
-			if (!self.settings.hideSelected) {
-				for (i = 0, n = self.items.length; i < n; i++) {
-					self.getOption(self.items[i]).addClass('selected');
-				}
-			}
-	
-			// add create option
-			has_create_option = self.canCreate(query);
-			if (has_create_option) {
-				$dropdown_content.prepend(self.render('option_create', {input: query}));
-				$create = $($dropdown_content[0].childNodes[0]);
-			}
-	
-			// activate
-			self.hasOptions = results.items.length > 0 || has_create_option;
-			if (self.hasOptions) {
-				if (results.items.length > 0) {
-					$active_before = active_before && self.getOption(active_before);
-					if ($active_before && $active_before.length) {
-						$active = $active_before;
-					} else if (self.settings.mode === 'single' && self.items.length) {
-						$active = self.getOption(self.items[0]);
-					}
-					if (!$active || !$active.length) {
-						if ($create && !self.settings.addPrecedence) {
-							$active = self.getAdjacentOption($create, 1);
-						} else {
-							$active = $dropdown_content.find('[data-selectable]:first');
-						}
-					}
-				} else {
-					$active = $create;
-				}
-				self.setActiveOption($active);
-				if (triggerDropdown && !self.isOpen) { self.open(); }
-			} else {
-				self.setActiveOption(null);
-				if (triggerDropdown && self.isOpen) { self.close(); }
-			}
-		},
-	
-		/**
-		 * Adds an available option. If it already exists,
-		 * nothing will happen. Note: this does not refresh
-		 * the options list dropdown (use `refreshOptions`
-		 * for that).
-		 *
-		 * Usage:
-		 *
-		 *   this.addOption(data)
-		 *
-		 * @param {object|array} data
-		 */
-		addOption: function(data) {
-			var i, n, value, self = this;
-	
-			if ($.isArray(data)) {
-				for (i = 0, n = data.length; i < n; i++) {
-					self.addOption(data[i]);
-				}
-				return;
-			}
-	
-			if (value = self.registerOption(data)) {
-				self.userOptions[value] = true;
-				self.lastQuery = null;
-				self.trigger('option_add', value, data);
-			}
-		},
-	
-		/**
-		 * Registers an option to the pool of options.
-		 *
-		 * @param {object} data
-		 * @return {boolean|string}
-		 */
-		registerOption: function(data) {
-			var key = hash_key(data[this.settings.valueField]);
-			if (!key || this.options.hasOwnProperty(key)) return false;
-			data.$order = data.$order || ++this.order;
-			this.options[key] = data;
-			return key;
-		},
-	
-		/**
-		 * Registers an option group to the pool of option groups.
-		 *
-		 * @param {object} data
-		 * @return {boolean|string}
-		 */
-		registerOptionGroup: function(data) {
-			var key = hash_key(data[this.settings.optgroupValueField]);
-			if (!key) return false;
-	
-			data.$order = data.$order || ++this.order;
-			this.optgroups[key] = data;
-			return key;
-		},
-	
-		/**
-		 * Registers a new optgroup for options
-		 * to be bucketed into.
-		 *
-		 * @param {string} id
-		 * @param {object} data
-		 */
-		addOptionGroup: function(id, data) {
-			data[this.settings.optgroupValueField] = id;
-			if (id = this.registerOptionGroup(data)) {
-				this.trigger('optgroup_add', id, data);
-			}
-		},
-	
-		/**
-		 * Removes an existing option group.
-		 *
-		 * @param {string} id
-		 */
-		removeOptionGroup: function(id) {
-			if (this.optgroups.hasOwnProperty(id)) {
-				delete this.optgroups[id];
-				this.renderCache = {};
-				this.trigger('optgroup_remove', id);
-			}
-		},
-	
-		/**
-		 * Clears all existing option groups.
-		 */
-		clearOptionGroups: function() {
-			this.optgroups = {};
-			this.renderCache = {};
-			this.trigger('optgroup_clear');
-		},
-	
-		/**
-		 * Updates an option available for selection. If
-		 * it is visible in the selected items or options
-		 * dropdown, it will be re-rendered automatically.
-		 *
-		 * @param {string} value
-		 * @param {object} data
-		 */
-		updateOption: function(value, data) {
-			var self = this;
-			var $item, $item_new;
-			var value_new, index_item, cache_items, cache_options, order_old;
-	
-			value     = hash_key(value);
-			value_new = hash_key(data[self.settings.valueField]);
-	
-			// sanity checks
-			if (value === null) return;
-			if (!self.options.hasOwnProperty(value)) return;
-			if (typeof value_new !== 'string') throw new Error('Value must be set in option data');
-	
-			order_old = self.options[value].$order;
-	
-			// update references
-			if (value_new !== value) {
-				delete self.options[value];
-				index_item = self.items.indexOf(value);
-				if (index_item !== -1) {
-					self.items.splice(index_item, 1, value_new);
-				}
-			}
-			data.$order = data.$order || order_old;
-			self.options[value_new] = data;
-	
-			// invalidate render cache
-			cache_items = self.renderCache['item'];
-			cache_options = self.renderCache['option'];
-	
-			if (cache_items) {
-				delete cache_items[value];
-				delete cache_items[value_new];
-			}
-			if (cache_options) {
-				delete cache_options[value];
-				delete cache_options[value_new];
-			}
-	
-			// update the item if it's selected
-			if (self.items.indexOf(value_new) !== -1) {
-				$item = self.getItem(value);
-				$item_new = $(self.render('item', data));
-				if ($item.hasClass('active')) $item_new.addClass('active');
-				$item.replaceWith($item_new);
-			}
-	
-			// invalidate last query because we might have updated the sortField
-			self.lastQuery = null;
-	
-			// update dropdown contents
-			if (self.isOpen) {
-				self.refreshOptions(false);
-			}
-		},
-	
-		/**
-		 * Removes a single option.
-		 *
-		 * @param {string} value
-		 * @param {boolean} silent
-		 */
-		removeOption: function(value, silent) {
-			var self = this;
-			value = hash_key(value);
-	
-			var cache_items = self.renderCache['item'];
-			var cache_options = self.renderCache['option'];
-			if (cache_items) delete cache_items[value];
-			if (cache_options) delete cache_options[value];
-	
-			delete self.userOptions[value];
-			delete self.options[value];
-			self.lastQuery = null;
-			self.trigger('option_remove', value);
-			self.removeItem(value, silent);
-		},
-	
-		/**
-		 * Clears all options.
-		 */
-		clearOptions: function() {
-			var self = this;
-	
-			self.loadedSearches = {};
-			self.userOptions = {};
-			self.renderCache = {};
-			self.options = self.sifter.items = {};
-			self.lastQuery = null;
-			self.trigger('option_clear');
-			self.clear();
-		},
-	
-		/**
-		 * Returns the jQuery element of the option
-		 * matching the given value.
-		 *
-		 * @param {string} value
-		 * @returns {object}
-		 */
-		getOption: function(value) {
-			return this.getElementWithValue(value, this.$dropdown_content.find('[data-selectable]'));
-		},
-	
-		/**
-		 * Returns the jQuery element of the next or
-		 * previous selectable option.
-		 *
-		 * @param {object} $option
-		 * @param {int} direction  can be 1 for next or -1 for previous
-		 * @return {object}
-		 */
-		getAdjacentOption: function($option, direction) {
-			var $options = this.$dropdown.find('[data-selectable]');
-			var index    = $options.index($option) + direction;
-	
-			return index >= 0 && index < $options.length ? $options.eq(index) : $();
-		},
-	
-		/**
-		 * Finds the first element with a "data-value" attribute
-		 * that matches the given value.
-		 *
-		 * @param {mixed} value
-		 * @param {object} $els
-		 * @return {object}
-		 */
-		getElementWithValue: function(value, $els) {
-			value = hash_key(value);
-	
-			if (typeof value !== 'undefined' && value !== null) {
-				for (var i = 0, n = $els.length; i < n; i++) {
-					if ($els[i].getAttribute('data-value') === value) {
-						return $($els[i]);
-					}
-				}
-			}
-	
-			return $();
-		},
-	
-		/**
-		 * Returns the jQuery element of the item
-		 * matching the given value.
-		 *
-		 * @param {string} value
-		 * @returns {object}
-		 */
-		getItem: function(value) {
-			return this.getElementWithValue(value, this.$control.children());
-		},
-	
-		/**
-		 * "Selects" multiple items at once. Adds them to the list
-		 * at the current caret position.
-		 *
-		 * @param {string} value
-		 * @param {boolean} silent
-		 */
-		addItems: function(values, silent) {
-			var items = $.isArray(values) ? values : [values];
-			for (var i = 0, n = items.length; i < n; i++) {
-				this.isPending = (i < n - 1);
-				this.addItem(items[i], silent);
-			}
-		},
-	
-		/**
-		 * "Selects" an item. Adds it to the list
-		 * at the current caret position.
-		 *
-		 * @param {string} value
-		 * @param {boolean} silent
-		 */
-		addItem: function(value, silent) {
-			var events = silent ? [] : ['change'];
-	
-			debounce_events(this, events, function() {
-				var $item, $option, $options;
-				var self = this;
-				var inputMode = self.settings.mode;
-				var i, active, value_next, wasFull;
-				value = hash_key(value);
-	
-				if (self.items.indexOf(value) !== -1) {
-					if (inputMode === 'single') self.close();
-					return;
-				}
-	
-				if (!self.options.hasOwnProperty(value)) return;
-				if (inputMode === 'single') self.clear(silent);
-				if (inputMode === 'multi' && self.isFull()) return;
-	
-				$item = $(self.render('item', self.options[value]));
-				wasFull = self.isFull();
-				self.items.splice(self.caretPos, 0, value);
-				self.insertAtCaret($item);
-				if (!self.isPending || (!wasFull && self.isFull())) {
-					self.refreshState();
-				}
-	
-				if (self.isSetup) {
-					$options = self.$dropdown_content.find('[data-selectable]');
-	
-					// update menu / remove the option (if this is not one item being added as part of series)
-					if (!self.isPending) {
-						$option = self.getOption(value);
-						value_next = self.getAdjacentOption($option, 1).attr('data-value');
-						self.refreshOptions(self.isFocused && inputMode !== 'single');
-						if (value_next) {
-							self.setActiveOption(self.getOption(value_next));
-						}
-					}
-	
-					// hide the menu if the maximum number of items have been selected or no options are left
-					if (!$options.length || self.isFull()) {
-						self.close();
-					} else {
-						self.positionDropdown();
-					}
-	
-					self.updatePlaceholder();
-					self.trigger('item_add', value, $item);
-					self.updateOriginalInput({silent: silent});
-				}
-			});
-		},
-	
-		/**
-		 * Removes the selected item matching
-		 * the provided value.
-		 *
-		 * @param {string} value
-		 */
-		removeItem: function(value, silent) {
-			var self = this;
-			var $item, i, idx;
-	
-			$item = (typeof value === 'object') ? value : self.getItem(value);
-			value = hash_key($item.attr('data-value'));
-			i = self.items.indexOf(value);
-	
-			if (i !== -1) {
-				$item.remove();
-				if ($item.hasClass('active')) {
-					idx = self.$activeItems.indexOf($item[0]);
-					self.$activeItems.splice(idx, 1);
-				}
-	
-				self.items.splice(i, 1);
-				self.lastQuery = null;
-				if (!self.settings.persist && self.userOptions.hasOwnProperty(value)) {
-					self.removeOption(value, silent);
-				}
-	
-				if (i < self.caretPos) {
-					self.setCaret(self.caretPos - 1);
-				}
-	
-				self.refreshState();
-				self.updatePlaceholder();
-				self.updateOriginalInput({silent: silent});
-				self.positionDropdown();
-				self.trigger('item_remove', value, $item);
-			}
-		},
-	
-		/**
-		 * Invokes the `create` method provided in the
-		 * selectize options that should provide the data
-		 * for the new item, given the user input.
-		 *
-		 * Once this completes, it will be added
-		 * to the item list.
-		 *
-		 * @param {string} value
-		 * @param {boolean} [triggerDropdown]
-		 * @param {function} [callback]
-		 * @return {boolean}
-		 */
-		createItem: function(input, triggerDropdown) {
-			var self  = this;
-			var caret = self.caretPos;
-			input = input || $.trim(self.$control_input.val() || '');
-	
-			var callback = arguments[arguments.length - 1];
-			if (typeof callback !== 'function') callback = function() {};
-	
-			if (typeof triggerDropdown !== 'boolean') {
-				triggerDropdown = true;
-			}
-	
-			if (!self.canCreate(input)) {
-				callback();
-				return false;
-			}
-	
-			self.lock();
-	
-			var setup = (typeof self.settings.create === 'function') ? this.settings.create : function(input) {
-				var data = {};
-				data[self.settings.labelField] = input;
-				data[self.settings.valueField] = input;
-				return data;
-			};
-	
-			var create = once(function(data) {
-				self.unlock();
-	
-				if (!data || typeof data !== 'object') return callback();
-				var value = hash_key(data[self.settings.valueField]);
-				if (typeof value !== 'string') return callback();
-	
-				self.setTextboxValue('');
-				self.addOption(data);
-				self.setCaret(caret);
-				self.addItem(value);
-				self.refreshOptions(triggerDropdown && self.settings.mode !== 'single');
-				callback(data);
-			});
-	
-			var output = setup.apply(this, [input, create]);
-			if (typeof output !== 'undefined') {
-				create(output);
-			}
-	
-			return true;
-		},
-	
-		/**
-		 * Re-renders the selected item lists.
-		 */
-		refreshItems: function() {
-			this.lastQuery = null;
-	
-			if (this.isSetup) {
-				this.addItem(this.items);
-			}
-	
-			this.refreshState();
-			this.updateOriginalInput();
-		},
-	
-		/**
-		 * Updates all state-dependent attributes
-		 * and CSS classes.
-		 */
-		refreshState: function() {
-			var invalid, self = this;
-			if (self.isRequired) {
-				if (self.items.length) self.isInvalid = false;
-				self.$control_input.prop('required', invalid);
-			}
-			self.refreshClasses();
-		},
-	
-		/**
-		 * Updates all state-dependent CSS classes.
-		 */
-		refreshClasses: function() {
-			var self     = this;
-			var isFull   = self.isFull();
-			var isLocked = self.isLocked;
-	
-			self.$wrapper
-				.toggleClass('rtl', self.rtl);
-	
-			self.$control
-				.toggleClass('focus', self.isFocused)
-				.toggleClass('disabled', self.isDisabled)
-				.toggleClass('required', self.isRequired)
-				.toggleClass('invalid', self.isInvalid)
-				.toggleClass('locked', isLocked)
-				.toggleClass('full', isFull).toggleClass('not-full', !isFull)
-				.toggleClass('input-active', self.isFocused && !self.isInputHidden)
-				.toggleClass('dropdown-active', self.isOpen)
-				.toggleClass('has-options', !$.isEmptyObject(self.options))
-				.toggleClass('has-items', self.items.length > 0);
-	
-			self.$control_input.data('grow', !isFull && !isLocked);
-		},
-	
-		/**
-		 * Determines whether or not more items can be added
-		 * to the control without exceeding the user-defined maximum.
-		 *
-		 * @returns {boolean}
-		 */
-		isFull: function() {
-			return this.settings.maxItems !== null && this.items.length >= this.settings.maxItems;
-		},
-	
-		/**
-		 * Refreshes the original <select> or <input>
-		 * element to reflect the current state.
-		 */
-		updateOriginalInput: function(opts) {
-			var i, n, options, label, self = this;
-			opts = opts || {};
-	
-			if (self.tagType === TAG_SELECT) {
-				options = [];
-				for (i = 0, n = self.items.length; i < n; i++) {
-					label = self.options[self.items[i]][self.settings.labelField] || '';
-					options.push('<option value="' + escape_html(self.items[i]) + '" selected="selected">' + escape_html(label) + '</option>');
-				}
-				if (!options.length && !this.$input.attr('multiple')) {
-					options.push('<option value="" selected="selected"></option>');
-				}
-				self.$input.html(options.join(''));
-			} else {
-				self.$input.val(self.getValue());
-				self.$input.attr('value',self.$input.val());
-			}
-	
-			if (self.isSetup) {
-				if (!opts.silent) {
-					self.trigger('change', self.$input.val());
-				}
-			}
-		},
-	
-		/**
-		 * Shows/hide the input placeholder depending
-		 * on if there items in the list already.
-		 */
-		updatePlaceholder: function() {
-			if (!this.settings.placeholder) return;
-			var $input = this.$control_input;
-	
-			if (this.items.length) {
-				$input.removeAttr('placeholder');
-			} else {
-				$input.attr('placeholder', this.settings.placeholder);
-			}
-			$input.triggerHandler('update', {force: true});
-		},
-	
-		/**
-		 * Shows the autocomplete dropdown containing
-		 * the available options.
-		 */
-		open: function() {
-			var self = this;
-	
-			if (self.isLocked || self.isOpen || (self.settings.mode === 'multi' && self.isFull())) return;
-			self.focus();
-			self.isOpen = true;
-			self.refreshState();
-			self.$dropdown.css({visibility: 'hidden', display: 'block'});
-			self.positionDropdown();
-			self.$dropdown.css({visibility: 'visible'});
-			self.trigger('dropdown_open', self.$dropdown);
-		},
-	
-		/**
-		 * Closes the autocomplete dropdown menu.
-		 */
-		close: function() {
-			var self = this;
-			var trigger = self.isOpen;
-	
-			if (self.settings.mode === 'single' && self.items.length) {
-				self.hideInput();
-			}
-	
-			self.isOpen = false;
-			self.$dropdown.hide();
-			self.setActiveOption(null);
-			self.refreshState();
-	
-			if (trigger) self.trigger('dropdown_close', self.$dropdown);
-		},
-	
-		/**
-		 * Calculates and applies the appropriate
-		 * position of the dropdown.
-		 */
-		positionDropdown: function() {
-			var $control = this.$control;
-			var offset = this.settings.dropdownParent === 'body' ? $control.offset() : $control.position();
-			offset.top += $control.outerHeight(true);
-	
-			this.$dropdown.css({
-				width : $control.outerWidth(),
-				top   : offset.top,
-				left  : offset.left
-			});
-		},
-	
-		/**
-		 * Resets / clears all selected items
-		 * from the control.
-		 *
-		 * @param {boolean} silent
-		 */
-		clear: function(silent) {
-			var self = this;
-	
-			if (!self.items.length) return;
-			self.$control.children(':not(input)').remove();
-			self.items = [];
-			self.lastQuery = null;
-			self.setCaret(0);
-			self.setActiveItem(null);
-			self.updatePlaceholder();
-			self.updateOriginalInput({silent: silent});
-			self.refreshState();
-			self.showInput();
-			self.trigger('clear');
-		},
-	
-		/**
-		 * A helper method for inserting an element
-		 * at the current caret position.
-		 *
-		 * @param {object} $el
-		 */
-		insertAtCaret: function($el) {
-			var caret = Math.min(this.caretPos, this.items.length);
-			if (caret === 0) {
-				this.$control.prepend($el);
-			} else {
-				$(this.$control[0].childNodes[caret]).before($el);
-			}
-			this.setCaret(caret + 1);
-		},
-	
-		/**
-		 * Removes the current selected item(s).
-		 *
-		 * @param {object} e (optional)
-		 * @returns {boolean}
-		 */
-		deleteSelection: function(e) {
-			var i, n, direction, selection, values, caret, option_select, $option_select, $tail;
-			var self = this;
-	
-			direction = (e && e.keyCode === KEY_BACKSPACE) ? -1 : 1;
-			selection = getSelection(self.$control_input[0]);
-	
-			if (self.$activeOption && !self.settings.hideSelected) {
-				option_select = self.getAdjacentOption(self.$activeOption, -1).attr('data-value');
-			}
-	
-			// determine items that will be removed
-			values = [];
-	
-			if (self.$activeItems.length) {
-				$tail = self.$control.children('.active:' + (direction > 0 ? 'last' : 'first'));
-				caret = self.$control.children(':not(input)').index($tail);
-				if (direction > 0) { caret++; }
-	
-				for (i = 0, n = self.$activeItems.length; i < n; i++) {
-					values.push($(self.$activeItems[i]).attr('data-value'));
-				}
-				if (e) {
-					e.preventDefault();
-					e.stopPropagation();
-				}
-			} else if ((self.isFocused || self.settings.mode === 'single') && self.items.length) {
-				if (direction < 0 && selection.start === 0 && selection.length === 0) {
-					values.push(self.items[self.caretPos - 1]);
-				} else if (direction > 0 && selection.start === self.$control_input.val().length) {
-					values.push(self.items[self.caretPos]);
-				}
-			}
-	
-			// allow the callback to abort
-			if (!values.length || (typeof self.settings.onDelete === 'function' && self.settings.onDelete.apply(self, [values]) === false)) {
-				return false;
-			}
-	
-			// perform removal
-			if (typeof caret !== 'undefined') {
-				self.setCaret(caret);
-			}
-			while (values.length) {
-				self.removeItem(values.pop());
-			}
-	
-			self.showInput();
-			self.positionDropdown();
-			self.refreshOptions(true);
-	
-			// select previous option
-			if (option_select) {
-				$option_select = self.getOption(option_select);
-				if ($option_select.length) {
-					self.setActiveOption($option_select);
-				}
-			}
-	
-			return true;
-		},
-	
-		/**
-		 * Selects the previous / next item (depending
-		 * on the `direction` argument).
-		 *
-		 * > 0 - right
-		 * < 0 - left
-		 *
-		 * @param {int} direction
-		 * @param {object} e (optional)
-		 */
-		advanceSelection: function(direction, e) {
-			var tail, selection, idx, valueLength, cursorAtEdge, $tail;
-			var self = this;
-	
-			if (direction === 0) return;
-			if (self.rtl) direction *= -1;
-	
-			tail = direction > 0 ? 'last' : 'first';
-			selection = getSelection(self.$control_input[0]);
-	
-			if (self.isFocused && !self.isInputHidden) {
-				valueLength = self.$control_input.val().length;
-				cursorAtEdge = direction < 0
-					? selection.start === 0 && selection.length === 0
-					: selection.start === valueLength;
-	
-				if (cursorAtEdge && !valueLength) {
-					self.advanceCaret(direction, e);
-				}
-			} else {
-				$tail = self.$control.children('.active:' + tail);
-				if ($tail.length) {
-					idx = self.$control.children(':not(input)').index($tail);
-					self.setActiveItem(null);
-					self.setCaret(direction > 0 ? idx + 1 : idx);
-				}
-			}
-		},
-	
-		/**
-		 * Moves the caret left / right.
-		 *
-		 * @param {int} direction
-		 * @param {object} e (optional)
-		 */
-		advanceCaret: function(direction, e) {
-			var self = this, fn, $adj;
-	
-			if (direction === 0) return;
-	
-			fn = direction > 0 ? 'next' : 'prev';
-			if (self.isShiftDown) {
-				$adj = self.$control_input[fn]();
-				if ($adj.length) {
-					self.hideInput();
-					self.setActiveItem($adj);
-					e && e.preventDefault();
-				}
-			} else {
-				self.setCaret(self.caretPos + direction);
-			}
-		},
-	
-		/**
-		 * Moves the caret to the specified index.
-		 *
-		 * @param {int} i
-		 */
-		setCaret: function(i) {
-			var self = this;
-	
-			if (self.settings.mode === 'single') {
-				i = self.items.length;
-			} else {
-				i = Math.max(0, Math.min(self.items.length, i));
-			}
-	
-			if(!self.isPending) {
-				// the input must be moved by leaving it in place and moving the
-				// siblings, due to the fact that focus cannot be restored once lost
-				// on mobile webkit devices
-				var j, n, fn, $children, $child;
-				$children = self.$control.children(':not(input)');
-				for (j = 0, n = $children.length; j < n; j++) {
-					$child = $($children[j]).detach();
-					if (j <  i) {
-						self.$control_input.before($child);
-					} else {
-						self.$control.append($child);
-					}
-				}
-			}
-	
-			self.caretPos = i;
-		},
-	
-		/**
-		 * Disables user input on the control. Used while
-		 * items are being asynchronously created.
-		 */
-		lock: function() {
-			this.close();
-			this.isLocked = true;
-			this.refreshState();
-		},
-	
-		/**
-		 * Re-enables user input on the control.
-		 */
-		unlock: function() {
-			this.isLocked = false;
-			this.refreshState();
-		},
-	
-		/**
-		 * Disables user input on the control completely.
-		 * While disabled, it cannot receive focus.
-		 */
-		disable: function() {
-			var self = this;
-			self.$input.prop('disabled', true);
-			self.$control_input.prop('disabled', true).prop('tabindex', -1);
-			self.isDisabled = true;
-			self.lock();
-		},
-	
-		/**
-		 * Enables the control so that it can respond
-		 * to focus and user input.
-		 */
-		enable: function() {
-			var self = this;
-			self.$input.prop('disabled', false);
-			self.$control_input.prop('disabled', false).prop('tabindex', self.tabIndex);
-			self.isDisabled = false;
-			self.unlock();
-		},
-	
-		/**
-		 * Completely destroys the control and
-		 * unbinds all event listeners so that it can
-		 * be garbage collected.
-		 */
-		destroy: function() {
-			var self = this;
-			var eventNS = self.eventNS;
-			var revertSettings = self.revertSettings;
-	
-			self.trigger('destroy');
-			self.off();
-			self.$wrapper.remove();
-			self.$dropdown.remove();
-	
-			self.$input
-				.html('')
-				.append(revertSettings.$children)
-				.removeAttr('tabindex')
-				.removeClass('selectized')
-				.attr({tabindex: revertSettings.tabindex})
-				.show();
-	
-			self.$control_input.removeData('grow');
-			self.$input.removeData('selectize');
-	
-			$(window).off(eventNS);
-			$(document).off(eventNS);
-			$(document.body).off(eventNS);
-	
-			delete self.$input[0].selectize;
-		},
-	
-		/**
-		 * A helper method for rendering "item" and
-		 * "option" templates, given the data.
-		 *
-		 * @param {string} templateName
-		 * @param {object} data
-		 * @returns {string}
-		 */
-		render: function(templateName, data) {
-			var value, id, label;
-			var html = '';
-			var cache = false;
-			var self = this;
-			var regex_tag = /^[\t \r\n]*<([a-z][a-z0-9\-_]*(?:\:[a-z][a-z0-9\-_]*)?)/i;
-	
-			if (templateName === 'option' || templateName === 'item') {
-				value = hash_key(data[self.settings.valueField]);
-				cache = !!value;
-			}
-	
-			// pull markup from cache if it exists
-			if (cache) {
-				if (!isset(self.renderCache[templateName])) {
-					self.renderCache[templateName] = {};
-				}
-				if (self.renderCache[templateName].hasOwnProperty(value)) {
-					return self.renderCache[templateName][value];
-				}
-			}
-	
-			// render markup
-			html = self.settings.render[templateName].apply(this, [data, escape_html]);
-	
-			// add mandatory attributes
-			if (templateName === 'option' || templateName === 'option_create') {
-				html = html.replace(regex_tag, '<$1 data-selectable');
-			}
-			if (templateName === 'optgroup') {
-				id = data[self.settings.optgroupValueField] || '';
-				html = html.replace(regex_tag, '<$1 data-group="' + escape_replace(escape_html(id)) + '"');
-			}
-			if (templateName === 'option' || templateName === 'item') {
-				html = html.replace(regex_tag, '<$1 data-value="' + escape_replace(escape_html(value || '')) + '"');
-			}
-	
-			// update cache
-			if (cache) {
-				self.renderCache[templateName][value] = html;
-			}
-	
-			return html;
-		},
-	
-		/**
-		 * Clears the render cache for a template. If
-		 * no template is given, clears all render
-		 * caches.
-		 *
-		 * @param {string} templateName
-		 */
-		clearCache: function(templateName) {
-			var self = this;
-			if (typeof templateName === 'undefined') {
-				self.renderCache = {};
-			} else {
-				delete self.renderCache[templateName];
-			}
-		},
-	
-		/**
-		 * Determines whether or not to display the
-		 * create item prompt, given a user input.
-		 *
-		 * @param {string} input
-		 * @return {boolean}
-		 */
-		canCreate: function(input) {
-			var self = this;
-			if (!self.settings.create) return false;
-			var filter = self.settings.createFilter;
-			return input.length
-				&& (typeof filter !== 'function' || filter.apply(self, [input]))
-				&& (typeof filter !== 'string' || new RegExp(filter).test(input))
-				&& (!(filter instanceof RegExp) || filter.test(input));
-		}
-	
-	});
-	
-	
-	Selectize.count = 0;
-	Selectize.defaults = {
-		options: [],
-		optgroups: [],
-	
-		plugins: [],
-		delimiter: ',',
-		splitOn: null, // regexp or string for splitting up values from a paste command
-		persist: true,
-		diacritics: true,
-		create: false,
-		createOnBlur: false,
-		createFilter: null,
-		highlight: true,
-		openOnFocus: true,
-		maxOptions: 1000,
-		maxItems: null,
-		hideSelected: null,
-		addPrecedence: false,
-		selectOnTab: false,
-		preload: false,
-		allowEmptyOption: false,
-		closeAfterSelect: false,
-	
-		scrollDuration: 60,
-		loadThrottle: 300,
-		loadingClass: 'loading',
-	
-		dataAttr: 'data-data',
-		optgroupField: 'optgroup',
-		valueField: 'value',
-		labelField: 'text',
-		optgroupLabelField: 'label',
-		optgroupValueField: 'value',
-		lockOptgroupOrder: false,
-	
-		sortField: '$order',
-		searchField: ['text'],
-		searchConjunction: 'and',
-	
-		mode: null,
-		wrapperClass: 'selectize-control',
-		inputClass: 'selectize-input',
-		dropdownClass: 'selectize-dropdown',
-		dropdownContentClass: 'selectize-dropdown-content',
-	
-		dropdownParent: null,
-	
-		copyClassesToDropdown: true,
-	
-		/*
-		load                 : null, // function(query, callback) { ... }
-		score                : null, // function(search) { ... }
-		onInitialize         : null, // function() { ... }
-		onChange             : null, // function(value) { ... }
-		onItemAdd            : null, // function(value, $item) { ... }
-		onItemRemove         : null, // function(value) { ... }
-		onClear              : null, // function() { ... }
-		onOptionAdd          : null, // function(value, data) { ... }
-		onOptionRemove       : null, // function(value) { ... }
-		onOptionClear        : null, // function() { ... }
-		onOptionGroupAdd     : null, // function(id, data) { ... }
-		onOptionGroupRemove  : null, // function(id) { ... }
-		onOptionGroupClear   : null, // function() { ... }
-		onDropdownOpen       : null, // function($dropdown) { ... }
-		onDropdownClose      : null, // function($dropdown) { ... }
-		onType               : null, // function(str) { ... }
-		onDelete             : null, // function(values) { ... }
-		*/
-	
-		render: {
-			/*
-			item: null,
-			optgroup: null,
-			optgroup_header: null,
-			option: null,
-			option_create: null
-			*/
-		}
-	};
-	
-	
-	$.fn.selectize = function(settings_user) {
-		var defaults             = $.fn.selectize.defaults;
-		var settings             = $.extend({}, defaults, settings_user);
-		var attr_data            = settings.dataAttr;
-		var field_label          = settings.labelField;
-		var field_value          = settings.valueField;
-		var field_optgroup       = settings.optgroupField;
-		var field_optgroup_label = settings.optgroupLabelField;
-		var field_optgroup_value = settings.optgroupValueField;
-	
-		/**
-		 * Initializes selectize from a <input type="text"> element.
-		 *
-		 * @param {object} $input
-		 * @param {object} settings_element
-		 */
-		var init_textbox = function($input, settings_element) {
-			var i, n, values, option;
-	
-			var data_raw = $input.attr(attr_data);
-	
-			if (!data_raw) {
-				var value = $.trim($input.val() || '');
-				if (!settings.allowEmptyOption && !value.length) return;
-				values = value.split(settings.delimiter);
-				for (i = 0, n = values.length; i < n; i++) {
-					option = {};
-					option[field_label] = values[i];
-					option[field_value] = values[i];
-					settings_element.options.push(option);
-				}
-				settings_element.items = values;
-			} else {
-				settings_element.options = JSON.parse(data_raw);
-				for (i = 0, n = settings_element.options.length; i < n; i++) {
-					settings_element.items.push(settings_element.options[i][field_value]);
-				}
-			}
-		};
-	
-		/**
-		 * Initializes selectize from a <select> element.
-		 *
-		 * @param {object} $input
-		 * @param {object} settings_element
-		 */
-		var init_select = function($input, settings_element) {
-			var i, n, tagName, $children, order = 0;
-			var options = settings_element.options;
-			var optionsMap = {};
-	
-			var readData = function($el) {
-				var data = attr_data && $el.attr(attr_data);
-				if (typeof data === 'string' && data.length) {
-					return JSON.parse(data);
-				}
-				return null;
-			};
-	
-			var addOption = function($option, group) {
-				$option = $($option);
-	
-				var value = hash_key($option.attr('value'));
-				if (!value && !settings.allowEmptyOption) return;
-	
-				// if the option already exists, it's probably been
-				// duplicated in another optgroup. in this case, push
-				// the current group to the "optgroup" property on the
-				// existing option so that it's rendered in both places.
-				if (optionsMap.hasOwnProperty(value)) {
-					if (group) {
-						var arr = optionsMap[value][field_optgroup];
-						if (!arr) {
-							optionsMap[value][field_optgroup] = group;
-						} else if (!$.isArray(arr)) {
-							optionsMap[value][field_optgroup] = [arr, group];
-						} else {
-							arr.push(group);
-						}
-					}
-					return;
-				}
-	
-				var option             = readData($option) || {};
-				option[field_label]    = option[field_label] || $option.text();
-				option[field_value]    = option[field_value] || value;
-				option[field_optgroup] = option[field_optgroup] || group;
-	
-				optionsMap[value] = option;
-				options.push(option);
-	
-				if ($option.is(':selected')) {
-					settings_element.items.push(value);
-				}
-			};
-	
-			var addGroup = function($optgroup) {
-				var i, n, id, optgroup, $options;
-	
-				$optgroup = $($optgroup);
-				id = $optgroup.attr('label');
-	
-				if (id) {
-					optgroup = readData($optgroup) || {};
-					optgroup[field_optgroup_label] = id;
-					optgroup[field_optgroup_value] = id;
-					settings_element.optgroups.push(optgroup);
-				}
-	
-				$options = $('option', $optgroup);
-				for (i = 0, n = $options.length; i < n; i++) {
-					addOption($options[i], id);
-				}
-			};
-	
-			settings_element.maxItems = $input.attr('multiple') ? null : 1;
-	
-			$children = $input.children();
-			for (i = 0, n = $children.length; i < n; i++) {
-				tagName = $children[i].tagName.toLowerCase();
-				if (tagName === 'optgroup') {
-					addGroup($children[i]);
-				} else if (tagName === 'option') {
-					addOption($children[i]);
-				}
-			}
-		};
-	
-		return this.each(function() {
-			if (this.selectize) return;
-	
-			var instance;
-			var $input = $(this);
-			var tag_name = this.tagName.toLowerCase();
-			var placeholder = $input.attr('placeholder') || $input.attr('data-placeholder');
-			if (!placeholder && !settings.allowEmptyOption) {
-				placeholder = $input.children('option[value=""]').text();
-			}
-	
-			var settings_element = {
-				'placeholder' : placeholder,
-				'options'     : [],
-				'optgroups'   : [],
-				'items'       : []
-			};
-	
-			if (tag_name === 'select') {
-				init_select($input, settings_element);
-			} else {
-				init_textbox($input, settings_element);
-			}
-	
-			instance = new Selectize($input, $.extend(true, {}, defaults, settings_element, settings_user));
-		});
-	};
-	
-	$.fn.selectize.defaults = Selectize.defaults;
-	$.fn.selectize.support = {
-		validity: SUPPORTS_VALIDITY_API
-	};
-	
-	
-	Selectize.define('drag_drop', function(options) {
-		if (!$.fn.sortable) throw new Error('The "drag_drop" plugin requires jQuery UI "sortable".');
-		if (this.settings.mode !== 'multi') return;
-		var self = this;
-	
-		self.lock = (function() {
-			var original = self.lock;
-			return function() {
-				var sortable = self.$control.data('sortable');
-				if (sortable) sortable.disable();
-				return original.apply(self, arguments);
-			};
-		})();
-	
-		self.unlock = (function() {
-			var original = self.unlock;
-			return function() {
-				var sortable = self.$control.data('sortable');
-				if (sortable) sortable.enable();
-				return original.apply(self, arguments);
-			};
-		})();
-	
-		self.setup = (function() {
-			var original = self.setup;
-			return function() {
-				original.apply(this, arguments);
-	
-				var $control = self.$control.sortable({
-					items: '[data-value]',
-					forcePlaceholderSize: true,
-					disabled: self.isLocked,
-					start: function(e, ui) {
-						ui.placeholder.css('width', ui.helper.css('width'));
-						$control.css({overflow: 'visible'});
-					},
-					stop: function() {
-						$control.css({overflow: 'hidden'});
-						var active = self.$activeItems ? self.$activeItems.slice() : null;
-						var values = [];
-						$control.children('[data-value]').each(function() {
-							values.push($(this).attr('data-value'));
-						});
-						self.setValue(values);
-						self.setActiveItem(active);
-					}
-				});
-			};
-		})();
-	
-	});
-	
-	Selectize.define('dropdown_header', function(options) {
-		var self = this;
-	
-		options = $.extend({
-			title         : 'Untitled',
-			headerClass   : 'selectize-dropdown-header',
-			titleRowClass : 'selectize-dropdown-header-title',
-			labelClass    : 'selectize-dropdown-header-label',
-			closeClass    : 'selectize-dropdown-header-close',
-	
-			html: function(data) {
-				return (
-					'<div class="' + data.headerClass + '">' +
-						'<div class="' + data.titleRowClass + '">' +
-							'<span class="' + data.labelClass + '">' + data.title + '</span>' +
-							'<a href="javascript:void(0)" class="' + data.closeClass + '">&times;</a>' +
-						'</div>' +
-					'</div>'
-				);
-			}
-		}, options);
-	
-		self.setup = (function() {
-			var original = self.setup;
-			return function() {
-				original.apply(self, arguments);
-				self.$dropdown_header = $(options.html(options));
-				self.$dropdown.prepend(self.$dropdown_header);
-			};
-		})();
-	
-	});
-	
-	Selectize.define('optgroup_columns', function(options) {
-		var self = this;
-	
-		options = $.extend({
-			equalizeWidth  : true,
-			equalizeHeight : true
-		}, options);
-	
-		this.getAdjacentOption = function($option, direction) {
-			var $options = $option.closest('[data-group]').find('[data-selectable]');
-			var index    = $options.index($option) + direction;
-	
-			return index >= 0 && index < $options.length ? $options.eq(index) : $();
-		};
-	
-		this.onKeyDown = (function() {
-			var original = self.onKeyDown;
-			return function(e) {
-				var index, $option, $options, $optgroup;
-	
-				if (this.isOpen && (e.keyCode === KEY_LEFT || e.keyCode === KEY_RIGHT)) {
-					self.ignoreHover = true;
-					$optgroup = this.$activeOption.closest('[data-group]');
-					index = $optgroup.find('[data-selectable]').index(this.$activeOption);
-	
-					if(e.keyCode === KEY_LEFT) {
-						$optgroup = $optgroup.prev('[data-group]');
-					} else {
-						$optgroup = $optgroup.next('[data-group]');
-					}
-	
-					$options = $optgroup.find('[data-selectable]');
-					$option  = $options.eq(Math.min($options.length - 1, index));
-					if ($option.length) {
-						this.setActiveOption($option);
-					}
-					return;
-				}
-	
-				return original.apply(this, arguments);
-			};
-		})();
-	
-		var getScrollbarWidth = function() {
-			var div;
-			var width = getScrollbarWidth.width;
-			var doc = document;
-	
-			if (typeof width === 'undefined') {
-				div = doc.createElement('div');
-				div.innerHTML = '<div style="width:50px;height:50px;position:absolute;left:-50px;top:-50px;overflow:auto;"><div style="width:1px;height:100px;"></div></div>';
-				div = div.firstChild;
-				doc.body.appendChild(div);
-				width = getScrollbarWidth.width = div.offsetWidth - div.clientWidth;
-				doc.body.removeChild(div);
-			}
-			return width;
-		};
-	
-		var equalizeSizes = function() {
-			var i, n, height_max, width, width_last, width_parent, $optgroups;
-	
-			$optgroups = $('[data-group]', self.$dropdown_content);
-			n = $optgroups.length;
-			if (!n || !self.$dropdown_content.width()) return;
-	
-			if (options.equalizeHeight) {
-				height_max = 0;
-				for (i = 0; i < n; i++) {
-					height_max = Math.max(height_max, $optgroups.eq(i).height());
-				}
-				$optgroups.css({height: height_max});
-			}
-	
-			if (options.equalizeWidth) {
-				width_parent = self.$dropdown_content.innerWidth() - getScrollbarWidth();
-				width = Math.round(width_parent / n);
-				$optgroups.css({width: width});
-				if (n > 1) {
-					width_last = width_parent - width * (n - 1);
-					$optgroups.eq(n - 1).css({width: width_last});
-				}
-			}
-		};
-	
-		if (options.equalizeHeight || options.equalizeWidth) {
-			hook.after(this, 'positionDropdown', equalizeSizes);
-			hook.after(this, 'refreshOptions', equalizeSizes);
-		}
-	
-	
-	});
-	
-	Selectize.define('remove_button', function(options) {
-		if (this.settings.mode === 'single') return;
-	
-		options = $.extend({
-			label     : '&times;',
-			title     : 'Remove',
-			className : 'remove',
-			append    : true
-		}, options);
-	
-		var self = this;
-		var html = '<a href="javascript:void(0)" class="' + options.className + '" tabindex="-1" title="' + escape_html(options.title) + '">' + options.label + '</a>';
-	
-		/**
-		 * Appends an element as a child (with raw HTML).
-		 *
-		 * @param {string} html_container
-		 * @param {string} html_element
-		 * @return {string}
-		 */
-		var append = function(html_container, html_element) {
-			var pos = html_container.search(/(<\/[^>]+>\s*)$/);
-			return html_container.substring(0, pos) + html_element + html_container.substring(pos);
-		};
-	
-		this.setup = (function() {
-			var original = self.setup;
-			return function() {
-				// override the item rendering method to add the button to each
-				if (options.append) {
-					var render_item = self.settings.render.item;
-					self.settings.render.item = function(data) {
-						return append(render_item.apply(this, arguments), html);
-					};
-				}
-	
-				original.apply(this, arguments);
-	
-				// add event listener
-				this.$control.on('click', '.' + options.className, function(e) {
-					e.preventDefault();
-					if (self.isLocked) return;
-	
-					var $item = $(e.currentTarget).parent();
-					self.setActiveItem($item);
-					if (self.deleteSelection()) {
-						self.setCaret(self.items.length);
-					}
-				});
-	
-			};
-		})();
-	
-	});
-	
-	Selectize.define('restore_on_backspace', function(options) {
-		var self = this;
-	
-		options.text = options.text || function(option) {
-			return option[this.settings.labelField];
-		};
-	
-		this.onKeyDown = (function() {
-			var original = self.onKeyDown;
-			return function(e) {
-				var index, option;
-				if (e.keyCode === KEY_BACKSPACE && this.$control_input.val() === '' && !this.$activeItems.length) {
-					index = this.caretPos - 1;
-					if (index >= 0 && index < this.items.length) {
-						option = this.options[this.items[index]];
-						if (this.deleteSelection(e)) {
-							this.setTextboxValue(options.text.apply(this, [option]));
-							this.refreshOptions(true);
-						}
-						e.preventDefault();
-						return;
-					}
-				}
-				return original.apply(this, arguments);
-			};
-		})();
-	});
-	
-
-	return Selectize;
-}));
\ No newline at end of file
diff --git a/reviewboard/static/rb/css/pages/admin.less b/reviewboard/static/rb/css/pages/admin.less
index fde1324732af890bc2fc9743d6e5238d3397dc38..be936dfe0798bf5fe733f7d820d5737a59609a63 100644
--- a/reviewboard/static/rb/css/pages/admin.less
+++ b/reviewboard/static/rb/css/pages/admin.less
@@ -1302,7 +1302,7 @@ table#change-history {
  * Related object autocomplete
  ***************************************************************************/
 
-.admin .related-object-select {
+.admin .related-object-selector {
   display: inline-block;
   width: 35em;
 }
diff --git a/reviewboard/static/rb/css/ui/related-object-selector.less b/reviewboard/static/rb/css/ui/related-object-selector.less
deleted file mode 100644
index e09e3454858f075486075872bd3a17ba918dc3d6..0000000000000000000000000000000000000000
--- a/reviewboard/static/rb/css/ui/related-object-selector.less
+++ /dev/null
@@ -1,80 +0,0 @@
-@import (reference) "../defs.less";
-
-
-/***************************************************************************
- * Related object autocomplete
- ***************************************************************************/
-
-.related-object-options,
-.related-object-selected {
-  font-size: @admin_font_size;
-
-  img {
-    width: 20px;
-    height: 20px;
-    border-radius: 10px;
-    vertical-align: top;
-  }
-
-  span {
-    line-height: 20px;
-    vertical-align: top;
-  }
-
-  .title {
-    display: inline-block;
-    min-width: 20em;
-    font-size: @admin_font_size;
-  }
-
-  .description {
-    display: inline-block;
-    font-size: (@admin_font_size * 0.9);
-    padding: 0;
-  }
-}
-
-.related-object-select {
-  vertical-align: middle;
-
-  .related-object-selected {
-    border: 1px solid #b8b8b8;
-    border-radius: 3px;
-    box-shadow: inset 0 1px 0 rgba(0,0,0,0.05), 0 1px 0 rgba(255,255,255,0.8);
-    list-style: none;
-    margin: 0.5em 0 0 0;
-    max-height: 20em;
-    min-height: 3em;
-    overflow-y: auto;
-    padding: 0;
-
-    li {
-      cursor: default;
-      padding: 5px 8px; // To match the selectize style in the drop-down menu.
-      vertical-align: middle;
-
-      div {
-        display: inline-block;
-      }
-
-      .fa-close {
-        color: #ccc;
-        cursor: pointer;
-        float: right;
-
-        &:hover {
-          color: black;
-        }
-      }
-    }
-  }
-
-  .selectize-input {
-    font-size: @admin_font_size;
-    padding: 4px;
-
-    &::after {
-      right: 10px;
-    }
-  }
-}
diff --git a/reviewboard/static/rb/js/admin/tests/relatedGroupSelectorViewTests.es6.js b/reviewboard/static/rb/js/admin/tests/relatedGroupSelectorViewTests.es6.js
new file mode 100644
index 0000000000000000000000000000000000000000..11ef4e702b52177010b1fd8eaaa398ff31b908a1
--- /dev/null
+++ b/reviewboard/static/rb/js/admin/tests/relatedGroupSelectorViewTests.es6.js
@@ -0,0 +1,104 @@
+suite('rb/admin/views/relatedGroupSelectorView', function() {
+    describe('Rendering', function() {
+        it('when empty', function() {
+            let view = new RB.RelatedGroupSelectorView({
+                $input: $('<input id="id_groups" type="hidden">'),
+                initialOptions: [],
+                multivalued: true,
+                inviteOnly: false,
+            });
+            expect(view.options.multivalued).toBe(true);
+            expect(view.options.inviteOnly).toBe(false);
+            view.render();
+
+            expect(view.$el.find('.related-object-selected li').length)
+                .toBe(0);
+
+        });
+    });
+
+    describe('Rendering', function() {
+        it('with inital options', function() {
+            let view = new RB.RelatedGroupSelectorView({
+                $input: $('<input id="id_groups" type="hidden">'),
+                initialOptions: [{
+                    id: 1,
+                    display_name: "Test Repository 1",
+                    name: "test_repo_1",
+                    invite_only: false,
+                }, {
+                    id: 2,
+                    display_name: "Test Repository 2",
+                    name: "test_repo_2",
+                    invite_only: true,
+                }],
+                multivalued: true,
+                inviteOnly: true,
+            });
+            view.render();
+            expect(view.options.multivalued).toBe(true);
+            expect(view.options.inviteOnly).toBe(true);
+
+            expect(view.$el.find('.related-object-selected li').length)
+                .toBe(2);
+            expect(view.$el.siblings('#id_groups').val()).toBe('');
+            /* The input element value should be empty, since the widget will
+               not fill in the values from the objects if the objects are
+               passed through initialOptions. */
+            expect(view._selectedIDs.size).toBe(2);
+
+        });
+    });
+
+    describe('Select item', function() {
+        let view;
+
+        beforeEach(function(done) {
+            $testsScratch.append('<input id="id_groups" type="hidden">');
+            view = new RB.RelatedGroupSelectorView({
+                $input: $('#id_groups'),
+                initialOptions: [],
+                multivalued: true
+            });
+            view.render();
+
+            /* These are the fake users, that will be displayed in the
+               dropdown */
+            spyOn(view, 'loadOptions').and.callFake(function(query, callback) {
+                callback([{
+                    id: 1,
+                    display_name: "Test Repository 1",
+                    name: "test_repo_1",
+                    invite_only: false,
+                }, {
+                    id: 2,
+                    display_name: "Test Repository 2",
+                    name: "test_repo_2",
+                    invite_only: true,
+                }]);
+            });
+
+            $('select')[0].selectize.focus();
+            /* The focus() method is being called asynchronously, so it
+              doesn't actually call the loadOptions() method here
+              instantly. That's why I use setTimeout to wait for it to
+              finish. */
+            setTimeout(function() {
+                $testsScratch.find('div .selectize-input.items.not-full input').click();
+                done();
+            }, 4000);
+            /* I probably shouldn't be doing this, but I
+            don't know how else to get it to work. */
+        });
+
+        it('from dropdown', function(done) {
+            expect(view.loadOptions).toHaveBeenCalled();
+            $("div[data-value='test_repo_1']").click();
+            $("div[data-value='test_repo_2']").click();
+            expect(view.$el.siblings('#id_groups').val()).toBe('1,2');
+            done();
+        });
+    });
+
+
+});
\ No newline at end of file
diff --git a/reviewboard/static/rb/js/admin/tests/relatedRepoSelectorViewTests.es6.js b/reviewboard/static/rb/js/admin/tests/relatedRepoSelectorViewTests.es6.js
new file mode 100644
index 0000000000000000000000000000000000000000..1f12b90bc4697581cdd5fce2dfc9a17385e4b6a6
--- /dev/null
+++ b/reviewboard/static/rb/js/admin/tests/relatedRepoSelectorViewTests.es6.js
@@ -0,0 +1,92 @@
+suite('rb/admin/views/relatedRepoSelectorView', function() {
+    describe('Rendering', function() {
+        it('when empty', function() {
+            let view = new RB.RelatedRepoSelectorView({
+                $input: $('<input id="id_repos" type="hidden">'),
+                initialOptions: [],
+                multivalued: true
+            });
+            expect(view.options.multivalued).toBe(true);
+            view.render();
+
+            expect(view.$el.find('.related-object-selected li').length)
+                .toBe(0);
+
+        });
+    });
+
+    describe('Rendering', function() {
+        it('with inital options', function() {
+            let view = new RB.RelatedRepoSelectorView({
+                $input: $('<input id="id_repos" type="hidden">'),
+                initialOptions: [{
+                    id: 1,
+                    name: "Test Repository 1",
+                }, {
+                    id: 2,
+                    name: "Test Repository 2",
+                }],
+                multivalued: true
+            });
+            view.render();
+            expect(view.options.multivalued).toBe(true);
+
+            expect(view.$el.find('.related-object-selected li').length)
+                .toBe(2);
+            expect(view.$el.siblings('#id_repos').val()).toBe('');
+            /* The input element value should be empty, since the widget will
+               not fill in the values from the objects if the objects are
+               passed through initialOptions. */
+            expect(view._selectedIDs.size).toBe(2);
+
+        });
+    });
+
+    describe('Select item', function() {
+        let view;
+
+        beforeEach(function(done) {
+            $testsScratch.append('<input id="id_repos" type="hidden">');
+            view = new RB.RelatedRepoSelectorView({
+                $input: $('#id_repos'),
+                initialOptions: [],
+                multivalued: true
+            });
+            view.render();
+
+            /* These are the fake users, that will be displayed in the
+               dropdown */
+            spyOn(view, 'loadOptions').and.callFake(function(query, callback) {
+                callback([{
+                    id: 1,
+                    name: "Test Repository 1",
+                }, {
+                    id: 2,
+                    name: "Test Repository 2",
+                }]);
+            });
+
+            $('select')[0].selectize.focus();
+            /* The focus() method is being called asynchronously, so it
+              doesn't actually call the loadOptions() method here
+              instantly. That's why I use setTimeout to wait for it to
+              finish. */
+            setTimeout(function() {
+                $testsScratch.find('div .selectize-input.items.not-full input').click();
+                done();
+            }, 4000);
+            /* I probably shouldn't be doing this, but I
+            don't know how else to get it to work. */
+        });
+
+        it('from dropdown', function(done) {
+            expect(view.loadOptions).toHaveBeenCalled();
+            $("div[data-value='Test Repository 1']").click();
+            $("div[data-value='Test Repository 2']").click();
+            expect(view.$el.siblings('#id_repos').val()).toBe('1,2');
+            done();
+        });
+    });
+
+
+});
\ No newline at end of file
diff --git a/reviewboard/static/rb/js/admin/tests/relatedUserSelectorViewTests.es6.js b/reviewboard/static/rb/js/admin/tests/relatedUserSelectorViewTests.es6.js
new file mode 100644
index 0000000000000000000000000000000000000000..46267893becba79e31ad8f3a8d1d20fe477948c7
--- /dev/null
+++ b/reviewboard/static/rb/js/admin/tests/relatedUserSelectorViewTests.es6.js
@@ -0,0 +1,99 @@
+suite('rb/admin/views/relatedUserSelectorView', function() {
+    describe('Rendering', function() {
+        it('when empty', function() {
+            let view = new RB.RelatedUserSelectorView({
+                $input: $('<input id="id_people" type="hidden">'),
+                initialOptions: [],
+                useAvatars: true,
+                multivalued: true
+            });
+            expect(view.options.useAvatars).toBe(true);
+            expect(view.options.multivalued).toBe(true);
+            view.render();
+
+            expect(view.$el.find('.related-object-selected li').length)
+                .toBe(0);
+        });
+    });
+
+    describe('Rendering', function() {
+        it('with inital options', function() {
+            let view = new RB.RelatedUserSelectorView({
+                $input: $('<input id="id_people" type="hidden">'),
+                initialOptions: [{"username": "admin", "fullname":
+                "Admin User", "id": 1,
+                "avatarURL": "https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\\u0026d=mm"},
+                {"username": "doc", "fullname": "Doc Dwarf", "id": 2,
+                "avatarURL": "https://secure.gravatar.com/avatar/b0f1ae4342591db2695fb11313114b3e?s=40\\u0026d=mm"},
+                {"username": "dopey", "fullname": "Dopey Dwarf", "id": 3,
+                "avatarURL": "https://secure.gravatar.com/avatar/1a0098e6600792ea4f714aa205bf3f2b?s=40\\u0026d=mm"}],
+                useAvatars: true,
+                multivalued: true
+            });
+            view.render();
+            expect(view.options.useAvatars).toBe(true);
+            expect(view.options.multivalued).toBe(true);
+
+            expect(view.$el.find('.related-object-selected li').length)
+                .toBe(3);
+            expect(view.$el.siblings('#id_people').val()).toBe('');
+            /* The input element value should be empty, since the widget will
+               not fill in the values from the objects if the objects are
+               passed through initialOptions. */
+            expect(view._selectedIDs.size).toBe(3);
+        });
+    });
+
+    describe('Select item', function() {
+        let view;
+
+        beforeEach(function(done) {
+            $testsScratch.append('<input id="id_people" type="hidden">');
+            view = new RB.RelatedUserSelectorView({
+                $input: $('#id_people'),
+                initialOptions: [],
+                useAvatars: true,
+                multivalued: true
+            });
+            view.render();
+
+            /* These are the fake users, that will be displayed in the
+               dropdown */
+            spyOn(view, 'loadOptions').and.callFake(function(query, callback) {
+                callback([{
+                    avatarURL: "https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\\u0026d=mm",
+                    fullname: "Admin User",
+                    id: 1,
+                    username: "admin",
+                }, {
+                    avatarURL: "https://secure.gravatar.com/avatar/b0f1ae4342591db2695fb11313114b3e?s=40\\u0026d=mm",
+                    fullname: "Doc Dwarf",
+                    id: 2,
+                    username: "doc",
+                }]);
+            });
+
+            $('select')[0].selectize.focus();
+            /* The focus() method is being called asynchronously, so it
+              doesn't actually call the loadOptions() method here
+              instantly. That's why I use setTimeout to wait for it to
+              finish. */
+            setTimeout(function() {
+                $testsScratch.find('div .selectize-input.items.not-full input').click();
+                done();
+            }, 4000);
+            /* I probably shouldn't be doing this, but I
+            don't know how else to get it to work. */
+        });
+
+        it('from dropdown', function(done) {
+            expect(view.loadOptions).toHaveBeenCalled();
+            $("div[data-value='admin']").click();
+            $("div[data-value='doc']").click();
+            expect(view.$el.siblings('#id_people').val()).toBe('1,2');
+            done();
+        });
+    });
+
+
+});
\ No newline at end of file
diff --git a/reviewboard/static/rb/js/admin/views/relatedGroupSelectorView.es6.js b/reviewboard/static/rb/js/admin/views/relatedGroupSelectorView.es6.js
new file mode 100644
index 0000000000000000000000000000000000000000..3cce694166af70d7a76518dc58bcab88fa0b7875
--- /dev/null
+++ b/reviewboard/static/rb/js/admin/views/relatedGroupSelectorView.es6.js
@@ -0,0 +1,119 @@
+(function() {
+
+
+const optionTemplate = _.template(dedent`
+    <div>
+     <span class="title"><%- name %> : <%- display_name %></span>
+    </div>
+`);
+
+
+/**
+ * A widget to select related groups using search and autocomplete.
+ */
+RB.RelatedGroupSelectorView = Djblets.RelatedObjectSelectorView.extend({
+    searchPlaceholderText: gettext('Search for groups...'),
+
+    /**
+     * Initialize the view.
+     *
+     * Args:
+     *     options (object):
+     *         Options for the view.
+     *
+     * Option Args:
+     *     localSitePrefix (string):
+     *         The URL prefix for the local site, if any.
+     *
+     *     multivalued (boolean):
+     *         Whether or not the widget should allow selecting multuple
+     *         values.
+     *
+     *     inviteOnly (boolean):
+     *         Whether or not we want to only search for inviteOnly review
+     *         groups.
+     */
+    initialize(options) {
+        Djblets.RelatedObjectSelectorView.prototype.initialize.call(
+            this,
+            _.defaults({
+                selectizeOptions: {
+                    searchField: ['name', 'display_name'],
+                    sortField: [
+                        {field: 'name'},
+                        {field: 'display_name'},
+                    ],
+                    valueField: 'name',
+                }
+            }, options));
+
+        this._localSitePrefix = options.localSitePrefix || '';
+        this._inviteOnly = options.inviteOnly;
+    },
+
+    /**
+     * Render an option in the drop-down menu.
+     *
+     * Args:
+     *     item (object):
+     *         The item to render.
+     *
+     * Returns:
+     *     string:
+     *     HTML to insert into the drop-down menu.
+     */
+    renderOption(item) {
+        return optionTemplate(item);
+    },
+
+    /**
+     * Load options from the server.
+     *
+     * Args:
+     *     query (string):
+     *         The string typed in by the user.
+     *
+     *     callback (function):
+     *         A callback to be called once data has been loaded. This should
+     *         be passed an array of objects, each representing an option in
+     *         the drop-down.
+     */
+    loadOptions(query, callback) {
+        const params = {
+            'only-fields': 'invite_only,name,display_name,id',
+            displayname: 1,
+        };
+
+        if (query.length !== 0) {
+            params.q = query;
+        }
+
+        $.ajax({
+            type: 'GET',
+            url: `${SITE_ROOT}${this._localSitePrefix}api/groups/`,
+            data: params,
+            success: results => {
+                /* This is done because we cannot filter using invite_only in
+                the groups api. */
+                if (this._inviteOnly === true) {
+                    results.groups = results.groups.filter(obj => {
+                        return obj.invite_only;
+                    });
+                }
+                callback(results.groups.map(u => ({
+                    name: u.name,
+                    display_name: u.display_name,
+                    id: u.id,
+                    invite_only: u.invite_only
+                })));
+            },
+            error: (...args) => {
+                console.error('Group query failed', args);
+                callback();
+            },
+        });
+    },
+});
+
+
+})();
diff --git a/reviewboard/static/rb/js/admin/views/relatedObjectSelectorView.es6.js b/reviewboard/static/rb/js/admin/views/relatedObjectSelectorView.es6.js
deleted file mode 100644
index 7b1dfe2f924ba01896b02cb7e819d69a49e67ad2..0000000000000000000000000000000000000000
--- a/reviewboard/static/rb/js/admin/views/relatedObjectSelectorView.es6.js
+++ /dev/null
@@ -1,246 +0,0 @@
-/**
- * A widget to select related objects using search and autocomplete.
- *
- * This is particularly useful for models where there can be a ton of rows in
- * the database. The built-in admin widgets provide a pretty poor
- * experience--either populating the list with the entire contents of the
- * table, which is super slow, or just listing PKs, which isn't usable.
- */
-RB.RelatedObjectSelectorView = Backbone.View.extend({
-    className: 'related-object-select',
-
-    /**
-     * The search placeholder text.
-     *
-     * Subclasses should override this.
-     */
-    searchPlaceholderText: '',
-
-    /**
-     * The element template.
-     *
-     * Subclasses may override this to change rendering.
-     */
-    template: _.template(dedent`
-        <select placeholder="<%- searchPlaceholderText %>"
-                class="related-object-options"></select>
-        <% if (multivalued) { %>
-        <ul class="related-object-selected"></ul>
-        <% } %>
-    `),
-
-    /**
-     * Initialize the view.
-     *
-     * Args:
-     *     options (object):
-     *         Options for the view.
-     *
-     * Option Args:
-     *     $input (jQuery):
-     *         The ``<input>`` element which should be populated with the list
-     *         of selected item PKs.
-     *
-     *     initialOptions (Array of object):
-     *         The initially selected options.
-     *
-     *     multivalued (boolean):
-     *         Whether or not the widget should allow selecting multiple
-     *         values.
-     *
-     *     selectizeOptions (object):
-     *          Additional options to pass in to $.selectize.
-     */
-    initialize(options) {
-        this.options = options;
-        this._$input = options.$input;
-        this._selectizeOptions = options.selectizeOptions;
-        this._selectedIDs = new Map();
-
-        _.bindAll(this, 'renderOption');
-    },
-
-    /**
-     * Render the view.
-     *
-     * Returns:
-     *     RB.RelatedObjectSelectorView:
-     *     This object, for chaining.
-     */
-    render() {
-        const self = this;
-
-        this.$el.html(this.template({
-            searchPlaceholderText: this.searchPlaceholderText,
-            multivalued: this.options.multivalued,
-        }));
-
-        this._$selected = this.$('.related-object-selected');
-
-        const renderItem = this.options.multivalued
-                           ? () => ''
-                           : this.renderOption;
-
-        const selectizeOptions = _.defaults(this._selectizeOptions, {
-            copyClassesToDropdown: true,
-            dropdownParent: 'body',
-            preload: 'focus',
-            render: {
-                item: renderItem,
-                option: this.renderOption,
-            },
-            load(query, callback) {
-                self.loadOptions(
-                    query,
-                    data => callback(data.filter(
-                        item => !self._selectedIDs.has(item.id)
-                    ))
-                );
-            },
-            onChange(selected) {
-                if (selected) {
-                    self._onItemSelected(this.options[selected], true);
-
-                    if (self.options.multivalued) {
-                        this.removeOption(selected);
-                    }
-                }
-
-                if (self.options.multivalued) {
-                    this.clear();
-                }
-            },
-        });
-
-        if (!this.options.multivalued && this.options.initialOptions.length) {
-            const item = this.options.initialOptions[0];
-            selectizeOptions.options = this.options.initialOptions;
-            selectizeOptions.items = [item[selectizeOptions.valueField]];
-        }
-
-        this.$('select').selectize(selectizeOptions);
-
-        if (this.options.multivalued) {
-            this.options.initialOptions.forEach(
-                item => this._onItemSelected(item, false)
-            );
-        }
-
-        this._$input.after(this.$el);
-        return this;
-    },
-
-    /**
-     * Update the "official" ``<input>`` element.
-     *
-     * This copies the list of selected item IDs into the form field which will
-     * be submitted.
-     */
-    _updateInput() {
-        this._$input.val(Array.from(this._selectedIDs.keys()).join(','));
-    },
-
-    /**
-     * Callback for when an item is selected.
-     *
-     * Args:
-     *     item (object):
-     *         The newly-selected item.
-     *
-     *     addToInput (boolean):
-     *         Whether the ID of the item should be added to the ``<input>``
-     *         field.
-     *
-     *         This will be ``false`` when populating the visible list from the
-     *         value of the form field when the page is initially loaded, and
-     *         ``true`` when adding items interactively.
-     */
-    _onItemSelected(item, addToInput) {
-        if (this.options.multivalued) {
-            const $li = $('<li>').html(this.renderOption(item));
-            const $items = this._$selected.children();
-            const text = $li.text();
-
-            $('<span class="remove-item fa fa-close">')
-                .click(() => this._onItemRemoved($li, item))
-                .appendTo($li);
-
-            let attached = false;
-
-            for (let i = 0; i < $items.length; i++) {
-                const $item = $items.eq(i);
-
-                if ($item.text().localeCompare(text) > 0) {
-                    $item.before($li);
-                    attached = true;
-                    break;
-                }
-            }
-
-            if (!attached) {
-                $li.appendTo(this._$selected);
-            }
-
-            this._selectedIDs.set(item.id, item);
-
-            if (addToInput) {
-                this._updateInput();
-            }
-        } else {
-            this._selectedIDs = new Map([[item.id, item]]);
-            this._updateInput();
-        }
-    },
-
-    /**
-     * Callback for when an item is removed from the list.
-     *
-     * Args:
-     *     $li (jQuery):
-     *         The element representing the item in the selected list.
-     *
-     *     item (object):
-     *         The item being removed.
-     */
-    _onItemRemoved($li, item) {
-        $li.remove();
-        this._selectedIDs.delete(item.id);
-        this._updateInput();
-    },
-
-    /**
-     * Render an option in the drop-down menu.
-     *
-     * This should be overridden in order to render type-specific data.
-     *
-     * Args:
-     *     item (object):
-     *         The item to render.
-     *
-     * Returns:
-     *     string:
-     *     HTML to insert into the drop-down menu.
-     */
-    renderOption(/* item */) {
-        return '';
-    },
-
-    /**
-     * Load options from the server.
-     *
-     * This should be overridden in order to make whatever API requests are
-     * necessary.
-     *
-     * Args:
-     *     query (string):
-     *         The string typed in by the user.
-     *
-     *     callback (function):
-     *         A callback to be called once data has been loaded. This should
-     *         be passed an array of objects, each representing an option in
-     *         the drop-down.
-     */
-    loadOptions(query, callback) {
-        callback();
-    },
-});
diff --git a/reviewboard/static/rb/js/admin/views/relatedRepoSelectorView.es6.js b/reviewboard/static/rb/js/admin/views/relatedRepoSelectorView.es6.js
new file mode 100644
index 0000000000000000000000000000000000000000..a17c1cb07022cb44bafc3fa7fe698871e27a0bae
--- /dev/null
+++ b/reviewboard/static/rb/js/admin/views/relatedRepoSelectorView.es6.js
@@ -0,0 +1,102 @@
+(function() {
+
+const optionTemplate = _.template(dedent`
+    <div>
+     <span class="title"><%- name %></span>
+    </div>
+`);
+
+
+/**
+ * A widget to select related repositories using search and autocomplete.
+ */
+RB.RelatedRepoSelectorView = Djblets.RelatedObjectSelectorView.extend({
+    searchPlaceholderText: gettext('Search for repositories...'),
+
+    /**
+     * Initialize the view.
+     *
+     * Args:
+     *     options (object):
+     *         Options for the view.
+     *
+     * Option Args:
+     *     localSitePrefix (string):
+     *         The URL prefix for the Local Site, if any.
+     *
+     *     multivalued (boolean):
+     *         Whether or not the widget should allow selecting multiple
+     *         values.
+     */
+    initialize(options) {
+        Djblets.RelatedObjectSelectorView.prototype.initialize.call(
+            this,
+            _.defaults({
+                selectizeOptions: {
+                    searchField: ['name'],
+                    sortField: [
+                        {field: 'name'},
+                    ],
+                    valueField: 'name',
+                }
+            }, options));
+
+        this._localSitePrefix = options.localSitePrefix || '';
+    },
+
+    /**
+     * Render an option in the drop-down menu.
+     *
+     * Args:
+     *     item (object):
+     *         The item to render.
+     *
+     * Returns:
+     *     string:
+     *     HTML to insert into the drop-down menu.
+     */
+    renderOption(item) {
+        return optionTemplate(item);
+    },
+
+    /**
+     * Load options from the server.
+     *
+     * Args:
+     *     query (string):
+     *         The string typed in by the user.
+     *
+     *     callback (function):
+     *         A callback to be called once data has been loaded. This should
+     *         be passed an array of objects, each representing an option in
+     *         the drop-down.
+     */
+    loadOptions(query, callback) {
+        const params = {
+            'only-fields': 'name,id',
+        };
+
+        if (query.length !== 0) {
+            params.q = query;
+        }
+
+        $.ajax({
+            type: 'GET',
+            url: `${SITE_ROOT}${this._localSitePrefix}api/repositories/`,
+            data: params,
+            success: results => {
+                callback(results.repositories.map(u => ({
+                    name: u.name,
+                    id: u.id,
+                })));
+            },
+            error: (...args) => {
+                console.error('Repository query failed', args);
+                callback();
+            },
+        });
+    },
+});
+
+
+})();
diff --git a/reviewboard/static/rb/js/admin/views/relatedUserSelectorView.es6.js b/reviewboard/static/rb/js/admin/views/relatedUserSelectorView.es6.js
index bc321660eca4dcf279a2707efdd0abd482afaba5..89b3208508af1c81dcf29380664de6d5cf72dedf 100644
--- a/reviewboard/static/rb/js/admin/views/relatedUserSelectorView.es6.js
+++ b/reviewboard/static/rb/js/admin/views/relatedUserSelectorView.es6.js
@@ -18,7 +18,7 @@ const optionTemplate = _.template(dedent`
 /**
  * A widget to select related users using search and autocomplete.
  */
-RB.RelatedUserSelectorView = RB.RelatedObjectSelectorView.extend({
+RB.RelatedUserSelectorView = Djblets.RelatedObjectSelectorView.extend({
     searchPlaceholderText: gettext('Search for users...'),
 
     /**
@@ -40,7 +40,7 @@ RB.RelatedUserSelectorView = RB.RelatedObjectSelectorView.extend({
      *         Whether to show avatars. Off by default.
      */
     initialize(options) {
-        RB.RelatedObjectSelectorView.prototype.initialize.call(
+        Djblets.RelatedObjectSelectorView.prototype.initialize.call(
             this,
             _.defaults({
                 selectizeOptions: {
@@ -111,7 +111,7 @@ RB.RelatedUserSelectorView = RB.RelatedObjectSelectorView.extend({
                 })));
             },
             error(...args) {
-                console.log('User query failed', args);
+                console.error('User query failed', args);
                 callback();
             },
         });
diff --git a/reviewboard/staticbundles.py b/reviewboard/staticbundles.py
index c4b30d6b2de0cbe66b41969491425f50a86d98ad..c49667f17a6fce4b72dacbab5556689acf9861b3 100644
--- a/reviewboard/staticbundles.py
+++ b/reviewboard/staticbundles.py
@@ -28,7 +28,6 @@ PIPELINE_JAVASCRIPT = dict({
             'lib/js/jquery.timesince.js',
             'lib/js/moment-2.12.0.js',
             'lib/js/moment-timezone-0.5.2.js',
-            'lib/js/selectize-0.12.1.js',
             'lib/js/ui.autocomplete.js',
             'lib/js/codemirror-5.26.min.js',
         ),
@@ -54,6 +53,9 @@ PIPELINE_JAVASCRIPT = dict({
     },
     'js-tests': {
         'source_filenames': (
+            'rb/js/admin/tests/relatedGroupSelectorViewTests.es6.js',
+            'rb/js/admin/tests/relatedReposSelectorViewTests.es6.js',
+            'rb/js/admin/tests/relatedUserSelectorViewTests.es6.js',
             'rb/js/collections/tests/filteredCollectionTests.js',
             'rb/js/configForms/models/tests/resourceListItemModelTests.js',
             'rb/js/diffviewer/collections/tests/diffReviewableCollectionTests.es6.js',
@@ -387,8 +389,9 @@ PIPELINE_JAVASCRIPT = dict({
     },
     'widgets': {
         'source_filenames': (
-            'rb/js/admin/views/relatedObjectSelectorView.es6.js',
             'rb/js/admin/views/relatedUserSelectorView.es6.js',
+            'rb/js/admin/views/relatedRepoSelectorView.es6.js',
+            'rb/js/admin/views/relatedGroupSelectorView.es6.js',
         ),
         'output_filename': 'rb/js/widgets.min.js',
     },
@@ -401,7 +404,6 @@ PIPELINE_STYLESHEETS = dict({
             'lib/css/codemirror.css',
             'lib/css/jquery-ui-1.8.24.min.css',
             'lib/css/fontawesome.less',
-            'lib/css/selectize.default-0.12.1.css',
             'rb/css/assets/icons.less',
             'rb/css/layout/helpers.less',
             'rb/css/pages/base.less',
@@ -470,11 +472,4 @@ PIPELINE_STYLESHEETS = dict({
         'output_filename': 'rb/css/admin.min.css',
         'absolute_paths': False,
     },
-    'widgets': {
-        'source_filenames': (
-            'rb/css/ui/related-object-selector.less',
-        ),
-        'output_filename': 'rb/css/widgets.min.css',
-        'absolute_paths': False,
-    },
 }, **DJBLETS_PIPELINE_STYLESHEETS)
diff --git a/reviewboard/templates/accounts/edit_oauth_app.html b/reviewboard/templates/accounts/edit_oauth_app.html
index 696284dd87d3da546add7e36ed65b7afdabf79e6..34c8bbe5da9ec23a37bac76dc8447ac1e14defe9 100644
--- a/reviewboard/templates/accounts/edit_oauth_app.html
+++ b/reviewboard/templates/accounts/edit_oauth_app.html
@@ -4,7 +4,6 @@
 {% block scripts-post %}
 {{block.super}}
 {%  javascript 'djblets-forms' %}
-{%  javascript 'widgets' %}
 {%  javascript 'oauth-edit' %}
 {% endblock %}
 
@@ -13,7 +12,6 @@
  <link rel="stylesheet" type="text/css" href="{% static 'admin/css/forms.css' %}">
 {%  stylesheet 'account-page' %}
 {%  stylesheet 'djblets-forms' %}
-{%  stylesheet 'widgets' %}
 {% endblock %}
 
 {% block title %}{% spaceless %}
diff --git a/reviewboard/templates/admin/base_site.html b/reviewboard/templates/admin/base_site.html
index e697318d25ec367aa2e411fcf62ac608db22e2ec..48acd2f0bcab39f4b307b3f9cdb8bd3f084ae3c8 100644
--- a/reviewboard/templates/admin/base_site.html
+++ b/reviewboard/templates/admin/base_site.html
@@ -9,8 +9,9 @@
 
 {% block scripts-post %}
 {{block.super}}
-{%  javascript 'widgets' %}
+{%  javascript 'djblets-widgets' %}
 {%  javascript 'oauth-edit' %}
+{%  javascript 'widgets' %}
 {%  block admin_scripts_post %}{% endblock %}
 {% endblock %}
 
@@ -21,7 +22,7 @@
 
 {% block css %}
 {%  stylesheet 'admin' %}
-{%  stylesheet 'widgets' %}
+{%  stylesheet 'djblets-widgets' %}
 {%  block extrastyle %}{% endblock %}
 <!--[if lte IE 7]><link rel="stylesheet" type="text/css" href="{% block stylesheet_ie %}{% static "admin/css/ie.css" %}{% endblock %}" /><![endif]-->
 {%  if LANGUAGE_BIDI %}<link rel="stylesheet" type="text/css" href="{% block stylesheet_rtl %}{% static "admin/css/rtl.css" %}{% endblock %}" />{% endif %}
diff --git a/reviewboard/templates/admin/oauth/application/change_form.html b/reviewboard/templates/admin/oauth/application/change_form.html
index 42dd0bfaabf893e03d967d195eb552d8137f53a5..f34d9a70dc63938e7e2b89245780615b418a4226 100644
--- a/reviewboard/templates/admin/oauth/application/change_form.html
+++ b/reviewboard/templates/admin/oauth/application/change_form.html
@@ -4,11 +4,9 @@
 {% block scripts-post %}
 {{block.super}}
 {%  javascript 'djblets-forms' %}
-{%  javascript 'widgets' %}
 {% endblock %}
 
 {% block extrastyle %}
 {{block.super}}
 {%  stylesheet 'djblets-forms' %}
-{%  stylesheet 'widgets' %}
 {% endblock %}
diff --git a/reviewboard/templates/admin/related_group_widget.html b/reviewboard/templates/admin/related_group_widget.html
new file mode 100644
index 0000000000000000000000000000000000000000..1b6796cb6202adac437806551cdd438d7e499b69
--- /dev/null
+++ b/reviewboard/templates/admin/related_group_widget.html
@@ -0,0 +1,17 @@
+{% load djblets_js i18n %}
+
+{{input_html}}
+
+<script>
+$(function() {
+    var view = new RB.RelatedGroupSelectorView({
+        $input: $('#{{input_id|escapejs}}'),
+        initialOptions: {{groups|json_dumps}},
+{% if local_site_name %}
+        localSitePrefix: 's/{{local_site_name|escapejs}}/',
+{% endif %}
+        multivalued: {{multivalued|yesno:'true,false'}},
+        inviteOnly: {{invite_only|yesno:'true,false'}}
+    }).render();
+});
+</script>
diff --git a/reviewboard/templates/admin/related_repo_widget.html b/reviewboard/templates/admin/related_repo_widget.html
new file mode 100644
index 0000000000000000000000000000000000000000..3c9610eebc4d66f6f00b98e42fb54a8b2ee6c689
--- /dev/null
+++ b/reviewboard/templates/admin/related_repo_widget.html
@@ -0,0 +1,16 @@
+{% load djblets_js i18n %}
+
+{{input_html}}
+
+<script>
+$(function() {
+    var view = new RB.RelatedRepoSelectorView({
+        $input: $('#{{input_id|escapejs}}'),
+        initialOptions: {{repos|json_dumps}},
+{% if local_site_name %}
+        localSitePrefix: 's/{{local_site_name|escapejs}}/',
+{% endif %}
+        multivalued: {{multivalued|yesno:'true,false'}}
+    }).render();
+});
+</script>
diff --git a/reviewboard/templates/admin/related_user_widget.html b/reviewboard/templates/admin/related_user_widget.html
index 87296ff2f9b02c7cb3ae1fe016f439e0e7fae21a..edd381e4b09704e3ab6b47a98f5c9896ccbe43dc 100644
--- a/reviewboard/templates/admin/related_user_widget.html
+++ b/reviewboard/templates/admin/related_user_widget.html
@@ -1,6 +1,8 @@
 {% load djblets_js i18n %}
 
-<script type="text/javascript">
+{{input_html}}
+
+<script>
 $(function() {
     var view = new RB.RelatedUserSelectorView({
         $input: $('#{{input_id|escapejs}}'),
diff --git a/reviewboard/templates/js/tests_base.html b/reviewboard/templates/js/tests_base.html
index cb22b4dff96953e685564c5f1e45d53f0ca37cc6..f776f9ab928771867e4bca29b666a5a053f20174 100644
--- a/reviewboard/templates/js/tests_base.html
+++ b/reviewboard/templates/js/tests_base.html
@@ -12,11 +12,13 @@
 {% block scripts-post %}
 {%  javascript "djblets-config-forms" %}
 {%  javascript "djblets-forms" %}
+{%  javascript "djblets-widgets" %}
 {%  javascript "config-forms" %}
 {%  javascript "reviews" %}
 {%  javascript "review-request-page" %}
 {%  javascript "newReviewRequest" %}
 {%  javascript "js-test-libs" %}
+{%  javascript "widgets" %}
 
 {%  block scripts-test %}{% endblock %}
 
