├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── select2 ├── __init__.py ├── fields.py ├── forms.py ├── models │ ├── __init__.py │ ├── base.py │ └── descriptors.py ├── static │ └── select2 │ │ ├── css │ │ ├── select2-spinner.gif │ │ ├── select2.css │ │ ├── select2.png │ │ └── select2x2.png │ │ ├── js │ │ ├── jquery-1.7.2.js │ │ ├── select2.jquery_ready.js │ │ ├── select2.jquery_ui_sortable.js │ │ ├── select2.js │ │ └── select2.min.js │ │ └── select2 │ │ ├── LICENSE │ │ ├── README.md │ │ ├── component.json │ │ ├── release.sh │ │ ├── select2-spinner.gif │ │ ├── select2.css │ │ ├── select2.js │ │ ├── select2.png │ │ └── select2x2.png ├── urls.py ├── utils.py ├── views.py └── widgets.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── admin.py ├── manage.py ├── models.py ├── settings.py ├── test_admin.py └── urls.py └── tox.ini /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.7, 3.8, 3.9] 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | 20 | - name: Setup chromedriver 21 | uses: nanasess/setup-chromedriver@v1.0.5 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install tox tox-gh-actions 27 | 28 | - name: Test with tox 29 | run: | 30 | tox -- -v --selenosis-driver=chrome-headless || \ 31 | tox -- -v --selenosis-driver=chrome-headless || \ 32 | tox -- -v --selenosis-driver=chrome-headless 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | /*.egg-info 4 | *.pyc 5 | *.log 6 | *.egg 7 | *.db 8 | *.pid 9 | pip-log.txt 10 | .DS_Store 11 | *swp 12 | ghostdriver.log 13 | .tox 14 | venv/ 15 | .python-version 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is published under the BSD 2-Clause License as listed below. 2 | http://www.opensource.org/licenses/bsd-license.php 3 | 4 | Copyright (c) 2017, Atlantic Media 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include select2/static * 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-select2-forms 2 | #################### 3 | 4 | .. image:: https://travis-ci.org/theatlantic/django-select2-forms.svg?branch=master 5 | :target: https://travis-ci.org/theatlantic/django-select2-forms 6 | 7 | **django-select2-forms** is a project that makes available Django form 8 | fields that use the `Select2 javascript 9 | plugin `_. It was created by 10 | developers at `The Atlantic `_. 11 | 12 | .. contents:: Table of Contents: 13 | 14 | Support 15 | ======= 16 | 17 | Being that Django added select2 support in 2.0, we will support up to that version 18 | for compatibility purposes. 19 | 20 | * ~=v3.0: Python >=3.7,<3.9 | Django 2.2,3.1,3.2 (current release) 21 | 22 | Local Development & Testing 23 | =========================== 24 | 25 | The following steps should only need to be done once when you first begin: 26 | 27 | Install ``pyenv`` 28 | ----------------- 29 | 30 | These instructions assume that you have `Homebrew `_ installed, 31 | but not ``pyenv``. 32 | 33 | .. code:: bash 34 | 35 | brew install pyenv 36 | touch ~/.bash_profile 37 | 38 | Add the following line to your ``~/bash_profile`` or ``.zshrc``:: 39 | 40 | eval "$(pyenv init -)" 41 | 42 | Reload your shell: 43 | 44 | .. code:: bash 45 | 46 | . ~/.bash_profile 47 | 48 | or 49 | 50 | .. code:: bash 51 | 52 | . ~/.zshrc 53 | 54 | Python Repository Setup 55 | ----------------------- 56 | 57 | First, clone the repository and prep your Python environment: 58 | 59 | .. code:: bash 60 | 61 | git clone https://github.com/theatlantic/django-select2-forms.git 62 | pyenv install 3.7.2 63 | pyenv install 3.8.0 64 | pyenv install 3.9.0 65 | pyenv local 3.7.2 3.8.0 3.9.0 66 | python -V 67 | 68 | The output of the previous command should be ``Python 3.7.2``. 69 | 70 | Finally: 71 | 72 | .. code:: bash 73 | 74 | python -m venv venv 75 | 76 | Activate Your Environment 77 | ------------------------- 78 | 79 | From the base directory: 80 | 81 | .. code:: bash 82 | 83 | deactivate # ignore: -bash: deactivate: command not found 84 | . venv/bin/activate 85 | pip install -U tox 86 | 87 | Running tests 88 | ------------- 89 | 90 | If you have not already done so, set up your environment by chromedriver: 91 | 92 | .. code:: bash 93 | 94 | brew install --cask chromedriver 95 | 96 | Run all tests: 97 | 98 | .. code:: bash 99 | 100 | tox -- --selenosis-driver=chrome-headless 101 | 102 | Show all available ``tox`` commands: 103 | 104 | .. code:: bash 105 | 106 | tox -av 107 | 108 | Run only a specific environment: 109 | 110 | .. code:: bash 111 | 112 | tox -e -- --selenosis-driver=chrome-headless # example: tox -e py37-django22 113 | 114 | Only run a specific test: 115 | 116 | .. code:: bash 117 | 118 | tox -- pytest -k test_something --selenosis-driver=chrome-headless 119 | 120 | Run an arbitrary command in a specific environment: 121 | 122 | .. code:: bash 123 | 124 | tox -e py37-django22 -- python # runs the Python REPL in that environment 125 | 126 | Setup a development environment: 127 | 128 | .. code:: bash 129 | 130 | tox -e --develop -r 131 | . .tox//bin/activate 132 | 133 | Installation 134 | ============ 135 | 136 | The recommended way to install is with pip:: 137 | 138 | pip install django-select2-forms 139 | 140 | or, to install with pip from source:: 141 | 142 | pip install -e git+git://github.com/theatlantic/django-select2-forms.git#egg=django-select2-forms 143 | 144 | If the source is already checked out, use setuptools:: 145 | 146 | python setup.py develop 147 | 148 | Configuration 149 | ============= 150 | 151 | ``django-select2-forms`` serves static assets using 152 | `django.contrib.staticfiles `_, 153 | and so requires that ``"select2"`` be added to your settings' 154 | ``INSTALLED_APPS``: 155 | 156 | .. code-block:: python 157 | 158 | INSTALLED_APPS = ( 159 | # ... 160 | 'select2', 161 | ) 162 | 163 | To use django-select2-forms' ajax support, ``'select2.urls'`` must be 164 | included in your urls.py ``urlpatterns``: 165 | 166 | .. code-block:: python 167 | 168 | urlpatterns = patterns('', 169 | # ... 170 | url(r'^select2/', include('select2.urls')), 171 | ) 172 | 173 | Usage 174 | ===== 175 | 176 | The simplest way to use ``django-select2-forms`` is to use 177 | ``select2.fields.ForeignKey`` and ``select2.fields.ManyToManyField`` in 178 | place of ``django.db.models.ForeignKey`` and 179 | ``django.db.models.ManyToManyField``, respectively. These fields extend 180 | their django equivalents and take the same arguments, along with extra 181 | optional keyword arguments. 182 | 183 | select2.fields.ForeignKey examples 184 | ---------------------------------- 185 | 186 | In the following two examples, an "entry" is associated with only one 187 | author. The example below does not use ajax, but instead performs 188 | autocomplete filtering on the client-side using the ```` 189 | elements (the labels of which are drawn from ``Author.__str__()``) 190 | in an html ````. 191 | 192 | .. code-block:: python 193 | 194 | @python_2_unicode_compatible 195 | class Author(models.Model): 196 | name = models.CharField(max_length=100) 197 | 198 | def __str__(self): 199 | return self.name 200 | 201 | class Entry(models.Model): 202 | author = select2.fields.ForeignKey(Author, 203 | overlay="Choose an author...", 204 | on_delete=models.CASCADE) 205 | 206 | This more advanced example autocompletes via ajax using the 207 | ``Author.name`` field and limits the autocomplete search to 208 | ``Author.objects.filter(active=True)`` 209 | 210 | .. code-block:: python 211 | 212 | class Author(models.Model): 213 | name = models.CharField(max_length=100) 214 | active = models.BooleanField() 215 | 216 | class Entry(models.Model): 217 | author = select2.fields.ForeignKey(Author, 218 | limit_choices_to=models.Q(active=True), 219 | ajax=True, 220 | search_field='name', 221 | overlay="Choose an author...", 222 | js_options={ 223 | 'quiet_millis': 200, 224 | }, 225 | on_delete=models.CASCADE) 226 | 227 | select2.fields.ManyToManyField examples 228 | --------------------------------------- 229 | 230 | In the following basic example, entries can have more than one author. 231 | This example does not do author name lookup via ajax, but populates 232 | ```` elements in a ```` with ``Author.__unicode__()`` 233 | for labels. 234 | 235 | .. code-block:: python 236 | 237 | @python_2_unicode_compatible 238 | class Author(models.Model): 239 | name = models.CharField(max_length=100) 240 | 241 | def __str__(self): 242 | return self.name 243 | 244 | class Entry(models.Model): 245 | authors = select2.fields.ManyToManyField(Author) 246 | 247 | The following "kitchen sink" example allows authors to be ordered, and 248 | uses ajax to autocomplete on two variants of an author's name. 249 | 250 | .. code-block:: python 251 | 252 | from django.db import models 253 | from django.db.models import Q 254 | import select2.fields 255 | import select2.models 256 | 257 | class Author(models.Model): 258 | name = models.CharField(max_length=100) 259 | alt_name = models.CharField(max_length=100, blank=True, null=True) 260 | 261 | class Entry(models.Model): 262 | categories = select2.fields.ManyToManyField(Author, 263 | through='EntryAuthors', 264 | ajax=True, 265 | search_field=lambda q: Q(name__icontains=q) | Q(alt_name__icontains=q), 266 | sort_field='position', 267 | js_options={'quiet_millis': 200}) 268 | 269 | form field example 270 | ------------------ 271 | 272 | If you don't need to use the ajax features of ``django-select2-forms`` 273 | it is possible to use select2 on django forms without modifying your 274 | models. The select2 formfields exist in the ``select2.fields`` module 275 | and have the same class names as their standard django counterparts 276 | (``ChoiceField``, ``MultipleChoiceField``, ``ModelChoiceField``, 277 | ``ModelMultipleChoiceField``). Here is the first ``ForeignKey`` example 278 | above, done with django formfields. 279 | 280 | .. code-block:: python 281 | 282 | class AuthorManager(models.Manager): 283 | def as_choices(self): 284 | for author in self.all(): 285 | yield (author.pk, force_text(author)) 286 | 287 | @python_2_unicode_compatible 288 | class Author(models.Model): 289 | name = models.CharField(max_length=100) 290 | objects = AuthorManager() 291 | 292 | def __str__(self): 293 | return self.name 294 | 295 | class Entry(models.Model): 296 | author = models.ForeignKey(Author, on_delete=models.CASCADE) 297 | 298 | class EntryForm(forms.ModelForm): 299 | author = select2.fields.ChoiceField( 300 | choices=Author.objects.as_choices(), 301 | overlay="Choose an author...") 302 | 303 | class Meta: 304 | model = Entry 305 | 306 | License 307 | ======= 308 | 309 | The django code is licensed under the `Simplified BSD 310 | License `_ and is 311 | copyright The Atlantic Media Company. View the ``LICENSE`` file under 312 | the root directory for complete license and copyright information. 313 | 314 | The Select2 javascript library included is licensed under the `Apache 315 | Software Foundation License Version 316 | 2.0 `_. View the file 317 | ``select2/static/select2/select2/LICENSE`` for complete license and 318 | copyright information about the Select2 javascript library. 319 | -------------------------------------------------------------------------------- /select2/__init__.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | 3 | try: 4 | __version__ = pkg_resources.get_distribution('django-select2-forms').version 5 | except pkg_resources.DistributionNotFound: 6 | __version__ = None 7 | -------------------------------------------------------------------------------- /select2/fields.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django import forms 3 | from django.db import models 4 | from django.core.exceptions import ImproperlyConfigured, ValidationError, FieldDoesNotExist 5 | from django.forms.models import ModelChoiceIterator 6 | from django.utils.encoding import force_str 7 | from django.utils.functional import Promise 8 | try: 9 | from django.db.models.fields.related import lazy_related_operation 10 | except ImportError: 11 | lazy_related_operation = None 12 | from django.db.models.fields.related import add_lazy_relation 13 | else: 14 | add_lazy_relation = None 15 | 16 | from sortedm2m.fields import SortedManyToManyField 17 | from sortedm2m.forms import SortedMultipleChoiceField 18 | 19 | from .widgets import Select, SelectMultiple 20 | 21 | 22 | __all__ = ( 23 | 'Select2FieldMixin', 'Select2ModelFieldMixin', 'ChoiceField', 24 | 'MultipleChoiceField', 'ModelChoiceField', 'ModelMultipleChoiceField', 25 | 'ForeignKey', 'ManyToManyField',) 26 | 27 | 28 | def compat_add_lazy_relation(cls, field, relation, operation): 29 | if add_lazy_relation is not None: 30 | return add_lazy_relation(cls, field, relation, operation) 31 | 32 | # Rearrange args for new Apps.lazy_model_operation 33 | def function(local, related, field): 34 | return operation(field, related, local) 35 | 36 | lazy_related_operation(function, cls, relation, field=field) 37 | 38 | 39 | dj19 = bool(django.VERSION >= (1, 9)) 40 | compat_rel = lambda f: getattr(f, 'remote_field' if dj19 else 'rel') 41 | compat_rel_to = lambda f: getattr(compat_rel(f), 'model' if dj19 else 'to') 42 | 43 | 44 | class Select2FieldMixin(object): 45 | 46 | def __init__(self, *args, **kwargs): 47 | widget_kwargs = {} 48 | # The child field class can pass widget_kwargs as a dict. We use this 49 | # in MultipleChoiceField to ensure that the field's choices get passed 50 | # along to the widget. This is unnecessary for model fields since the 51 | # choices in that case are iterators wrapping the queryset. 52 | if 'widget_kwargs' in kwargs: 53 | widget_kwargs.update(kwargs.pop('widget_kwargs')) 54 | widget_kwarg_keys = ['overlay', 'js_options', 'sortable', 'ajax'] 55 | for k in widget_kwarg_keys: 56 | if k in kwargs: 57 | widget_kwargs[k] = kwargs.pop(k) 58 | widget = kwargs.pop('widget', None) 59 | if isinstance(widget, type): 60 | if not issubclass(widget, Select): 61 | widget = self.widget 62 | elif not isinstance(widget, Select): 63 | widget = self.widget 64 | if isinstance(widget, type): 65 | kwargs['widget'] = widget(**widget_kwargs) 66 | else: 67 | kwargs['widget'] = widget 68 | super(Select2FieldMixin, self).__init__(*args, **kwargs) 69 | 70 | @property 71 | def choices(self): 72 | """ 73 | When it's time to get the choices, if it was a lazy then figure it out 74 | now and memoize the result. 75 | """ 76 | if isinstance(self._choices, Promise): 77 | self._choices = list(self._choices) 78 | return self._choices 79 | 80 | @choices.setter 81 | def choices(self, value): 82 | self._set_choices(value) 83 | 84 | def _set_choices(self, value): 85 | self._choices = value 86 | 87 | 88 | class ChoiceField(Select2FieldMixin, forms.ChoiceField): 89 | 90 | widget = Select 91 | 92 | 93 | class MultipleChoiceField(Select2FieldMixin, forms.MultipleChoiceField): 94 | 95 | widget = SelectMultiple 96 | 97 | def __init__(self, *args, **kwargs): 98 | # Explicitly pass the choices kwarg to the widget. "widget_kwargs" 99 | # is not a standard Django Form Field kwarg, but we pop it off in 100 | # Select2FieldMixin.__init__ 101 | kwargs['widget_kwargs'] = kwargs.get('widget_kwargs') or {} 102 | if 'choices' in kwargs: 103 | kwargs['widget_kwargs']['choices'] = kwargs['choices'] 104 | super(MultipleChoiceField, self).__init__(*args, **kwargs) 105 | 106 | def has_changed(self, initial, data): 107 | widget = self.widget 108 | if not isinstance(widget, SelectMultiple) and hasattr(widget, 'widget'): 109 | widget = widget.widget 110 | if hasattr(widget, 'format_value'): 111 | initial = widget.format_value(initial) 112 | else: 113 | initial = widget._format_value(initial) 114 | return super(MultipleChoiceField, self).has_changed(initial, data) 115 | 116 | 117 | class Select2ModelFieldMixin(Select2FieldMixin): 118 | 119 | search_field = None 120 | case_sensitive = False 121 | 122 | choice_iterator_cls = ModelChoiceIterator 123 | 124 | def __init__(self, search_field=None, case_sensitive=False, *args, **kwargs): 125 | if search_field is None and kwargs.get('ajax'): 126 | raise TypeError( 127 | ("keyword argument 'search_field' is required for field " 128 | "%s <%s>") % (self.name, self.__class__.__name__)) 129 | self.search_field = search_field 130 | self.case_sensitive = case_sensitive 131 | self.name = kwargs.pop('name') 132 | self.model = kwargs.pop('model') 133 | self.choice_iterator_cls = kwargs.pop('choice_iterator_cls', self.choice_iterator_cls) 134 | super(Select2ModelFieldMixin, self).__init__(*args, **kwargs) 135 | 136 | def _get_choices(self): 137 | if hasattr(self, '_choices'): 138 | return self._choices 139 | return self.choice_iterator_cls(self) 140 | 141 | choices = property(_get_choices, forms.ChoiceField._set_choices) 142 | 143 | 144 | class ModelChoiceField(Select2ModelFieldMixin, forms.ModelChoiceField): 145 | 146 | widget = Select 147 | 148 | def __init__(self, *args, **kwargs): 149 | super(ModelChoiceField, self).__init__(*args, **kwargs) 150 | self.widget.field = self 151 | 152 | 153 | class ModelMultipleChoiceField(Select2ModelFieldMixin, SortedMultipleChoiceField): 154 | 155 | widget = SelectMultiple 156 | 157 | #: Instance of the field on the through table used for storing sort position 158 | sort_field = None 159 | 160 | def __init__(self, *args, **kwargs): 161 | self.sort_field = kwargs.pop('sort_field', self.sort_field) 162 | if self.sort_field is not None: 163 | kwargs['sortable'] = True 164 | super(ModelMultipleChoiceField, self).__init__(*args, **kwargs) 165 | self.widget.field = self 166 | 167 | def clean(self, value): 168 | if self.required and not value: 169 | raise ValidationError(self.error_messages['required']) 170 | elif not self.required and not value: 171 | return [] 172 | 173 | if isinstance(value, str): 174 | value = value.split(',') 175 | 176 | if not isinstance(value, (list, tuple)): 177 | raise ValidationError(self.error_messages['list']) 178 | 179 | key = self.to_field_name or 'pk' 180 | 181 | for pk in value: 182 | try: 183 | self.queryset.filter(**{key: pk}) 184 | except ValueError: 185 | raise ValidationError(self.error_messages['invalid_pk_value'] % pk) 186 | qs = self.queryset.filter(**{ 187 | ('%s__in' % key): value, 188 | }) 189 | pks = set([force_str(getattr(o, key)) for o in qs]) 190 | 191 | # Create a dictionary for storing the original order of the items 192 | # passed from the form 193 | pk_positions = {} 194 | 195 | for i, val in enumerate(value): 196 | pk = force_str(val) 197 | if pk not in pks: 198 | raise ValidationError(self.error_messages['invalid_choice'] % val) 199 | pk_positions[pk] = i 200 | 201 | if not self.sort_field: 202 | return qs 203 | else: 204 | # Iterate through the objects and set the sort field to its 205 | # position in the comma-separated request data. Then return 206 | # a list of objects sorted on the sort field. 207 | sort_value_field_name = self.sort_field.name 208 | objs = [] 209 | for i, obj in enumerate(qs): 210 | pk = force_str(getattr(obj, key)) 211 | setattr(obj, sort_value_field_name, pk_positions[pk]) 212 | objs.append(obj) 213 | return sorted(objs, key=lambda obj: getattr(obj, sort_value_field_name)) 214 | 215 | 216 | class RelatedFieldMixin(object): 217 | 218 | search_field = None 219 | js_options = None 220 | overlay = None 221 | case_sensitive = False 222 | ajax = False 223 | 224 | def __init__(self, *args, **kwargs): 225 | self.search_field = kwargs.pop('search_field', None) 226 | self.js_options = kwargs.pop('js_options', None) 227 | self.overlay = kwargs.pop('overlay', self.overlay) 228 | self.case_sensitive = kwargs.pop('case_sensitive', self.case_sensitive) 229 | self.ajax = kwargs.pop('ajax', self.ajax) 230 | super(RelatedFieldMixin, self).__init__(*args, **kwargs) 231 | 232 | def _get_queryset(self, db=None): 233 | return compat_rel_to(self)._default_manager.using(db).complex_filter( 234 | compat_rel(self).limit_choices_to) 235 | 236 | @property 237 | def queryset(self): 238 | return self._get_queryset() 239 | 240 | def formfield(self, **kwargs): 241 | db = kwargs.pop('using', None) 242 | defaults = { 243 | 'form_class': ModelChoiceField, 244 | 'queryset': self._get_queryset(db), 245 | 'js_options': self.js_options, 246 | 'search_field': self.search_field, 247 | 'ajax': self.ajax, 248 | 'name': self.name, 249 | 'model': self.model, 250 | } 251 | defaults.update(kwargs) 252 | if self.overlay is not None: 253 | defaults.update({'overlay': self.overlay}) 254 | 255 | # If initial is passed in, it's a list of related objects, but the 256 | # MultipleChoiceField takes a list of IDs. 257 | if defaults.get('initial') is not None: 258 | initial = defaults['initial'] 259 | if callable(initial): 260 | initial = initial() 261 | defaults['initial'] = [i._get_pk_val() for i in initial] 262 | return models.Field.formfield(self, **defaults) 263 | 264 | def contribute_to_related_class(self, cls, related): 265 | if not self.ajax: 266 | return super(RelatedFieldMixin, self).contribute_to_related_class(cls, related) 267 | if self.search_field is None: 268 | raise TypeError( 269 | ("keyword argument 'search_field' is required for field " 270 | "'%(field_name)s' of model %(app_label)s.%(object_name)s") % { 271 | 'field_name': self.name, 272 | 'app_label': self.model._meta.app_label, 273 | 'object_name': self.model._meta.object_name}) 274 | if not callable(self.search_field) and not isinstance(self.search_field, str): 275 | raise TypeError( 276 | ("keyword argument 'search_field' must be either callable or " 277 | "string on field '%(field_name)s' of model " 278 | "%(app_label)s.%(object_name)s") % { 279 | 'field_name': self.name, 280 | 'app_label': self.model._meta.app_label, 281 | 'object_name': self.model._meta.object_name}) 282 | if isinstance(self.search_field, str): 283 | try: 284 | opts = related.parent_model._meta 285 | except AttributeError: 286 | # Django 1.8 287 | opts = related.model._meta 288 | try: 289 | opts.get_field(self.search_field) 290 | except FieldDoesNotExist: 291 | raise ImproperlyConfigured( 292 | ("keyword argument 'search_field' references non-existent " 293 | "field '%(search_field)s' in %(field_name)s of model " 294 | "<%(app_label)s.%(object_name)s>") % { 295 | 'search_field': self.search_field, 296 | 'field_name': self.name, 297 | 'app_label': opts.app_label, 298 | 'object_name': opts.object_name}) 299 | super(RelatedFieldMixin, self).contribute_to_related_class(cls, related) 300 | 301 | 302 | class ForeignKey(RelatedFieldMixin, models.ForeignKey): 303 | 304 | def formfield(self, **kwargs): 305 | defaults = { 306 | 'to_field_name': compat_rel(self).field_name, 307 | } 308 | defaults.update(**kwargs) 309 | return super(ForeignKey, self).formfield(**defaults) 310 | 311 | 312 | class OneToOneField(RelatedFieldMixin, models.OneToOneField): 313 | 314 | def formfield(self, **kwargs): 315 | defaults = { 316 | 'to_field_name': compat_rel(self).field_name, 317 | } 318 | defaults.update(**kwargs) 319 | return super(OneToOneField, self).formfield(**defaults) 320 | 321 | 322 | class ManyToManyField(RelatedFieldMixin, SortedManyToManyField): 323 | 324 | #: Name of the field on the through table used for storing sort position 325 | sort_value_field_name = None 326 | 327 | #: Instance of the field on the through table used for storing sort position 328 | sort_field = None 329 | 330 | def __init__(self, *args, **kwargs): 331 | if 'sort_field' in kwargs: 332 | kwargs['sort_value_field_name'] = kwargs.pop('sort_field') 333 | if 'sorted' not in kwargs: 334 | kwargs['sorted'] = bool(kwargs.get('sort_value_field_name')) 335 | super(ManyToManyField, self).__init__(*args, **kwargs) 336 | 337 | def formfield(self, **kwargs): 338 | defaults = { 339 | 'form_class': ModelMultipleChoiceField, 340 | 'sort_field': self.sort_field, 341 | } 342 | defaults.update(**kwargs) 343 | return super(ManyToManyField, self).formfield(**defaults) 344 | 345 | def contribute_to_class(self, cls, name): 346 | """ 347 | Replace the descriptor with our custom descriptor, so that the 348 | position field (which is saved in the formfield clean()) gets saved 349 | """ 350 | super(ManyToManyField, self).contribute_to_class(cls, name) 351 | if self.sorted: 352 | def resolve_sort_field(field, model, cls): 353 | model._sort_field_name = field.sort_value_field_name 354 | field.sort_field = model._meta.get_field(field.sort_value_field_name) 355 | if isinstance(compat_rel(self).through, str): 356 | compat_add_lazy_relation(cls, self, compat_rel(self).through, resolve_sort_field) 357 | else: 358 | resolve_sort_field(self, compat_rel(self).through, cls) 359 | -------------------------------------------------------------------------------- /select2/forms.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file exists so that imports of widgets and fields can be done in an 3 | analogous manner to imports from the widgets and fields of django.forms, e.g.: 4 | 5 | from django import forms 6 | from select2 import forms as select2forms 7 | 8 | class MyForm(forms.Form): 9 | regular_select = forms.Select() 10 | select2_select = select2forms.Select() 11 | """ 12 | from . import fields 13 | from .fields import * 14 | 15 | from . import widgets 16 | from .widgets import * 17 | -------------------------------------------------------------------------------- /select2/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import SortableThroughModel 2 | -------------------------------------------------------------------------------- /select2/models/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import sys 3 | 4 | from django.db import models 5 | from django.apps import apps 6 | from django.db.models.base import ModelBase 7 | from django.utils.functional import SimpleLazyObject 8 | 9 | 10 | class SortableThroughModelBase(ModelBase): 11 | """ 12 | This model meta class sets Meta.auto_created to point to the model class 13 | which tricks django into thinking that a custom M2M through-table was 14 | auto-created by the ORM. 15 | """ 16 | 17 | def __new__(cls, name, bases, attrs): 18 | # This is the super __new__ of the parent class. If we directly 19 | # call the parent class we'll register the model, which we don't 20 | # want to do yet 21 | super_new = super(SortableThroughModelBase, cls).__new__ 22 | base_super_new = super(ModelBase, cls).__new__ 23 | 24 | parents = [b for b in bases if isinstance(b, SortableThroughModelBase)] 25 | if not parents: 26 | # If this isn't a subclass of Model, don't do anything special. 27 | return super_new(cls, name, bases, attrs) 28 | 29 | module = attrs.get('__module__') 30 | 31 | # Create a class for the purposes of grabbing attributes that would 32 | # be set from inheritance 33 | tmp_new_class = base_super_new(cls, name, bases, {'__module__': module}) 34 | 35 | attr_meta = attrs.get('Meta', None) 36 | if not attr_meta: 37 | meta = getattr(tmp_new_class, 'Meta', None) 38 | else: 39 | meta = attr_meta 40 | if meta is None: 41 | class Meta: pass 42 | meta = Meta 43 | meta.__module__ = module 44 | 45 | # Determine the app_label. We need it to call get_model() 46 | app_label = getattr(meta, 'app_label', None) 47 | if not app_label: 48 | # All of this logic is from the parent class 49 | module = attrs.get('__module__') 50 | new_class = base_super_new(cls, name, bases, {'__module__': module}) 51 | model_module = sys.modules[new_class.__module__] 52 | app_label = model_module.__name__.split('.')[-2] 53 | 54 | # Create a callbable using closure variables that returns 55 | # get_model() for this model 56 | def _get_model(): 57 | return apps.get_model(app_label, name, False) 58 | 59 | # Pass the callable to SimpleLazyObject 60 | lazy_model = SimpleLazyObject(_get_model) 61 | # And set the auto_created to a lazy-loaded model object 62 | # of the class currently being created 63 | setattr(meta, 'auto_created', lazy_model) 64 | 65 | attrs['Meta'] = meta 66 | 67 | return super_new(cls, name, bases, attrs) 68 | 69 | 70 | class SortableThroughModel(models.Model, metaclass=SortableThroughModelBase): 71 | 72 | class Meta: 73 | abstract = True 74 | -------------------------------------------------------------------------------- /select2/models/descriptors.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.db import router 3 | from django.db.models import signals 4 | try: 5 | from django.db.models.fields.related import ReverseManyRelatedObjectsDescriptor as ReverseManyToOneDescriptor 6 | except ImportError: 7 | from django.db.models.fields.related import ReverseManyToOneDescriptor 8 | from django.utils.functional import cached_property 9 | 10 | 11 | class SortableReverseManyRelatedObjectsDescriptor(ReverseManyToOneDescriptor): 12 | 13 | @cached_property 14 | def related_manager_cls(self): 15 | ManyRelatedManagerBase = super( 16 | SortableReverseManyRelatedObjectsDescriptor, self).related_manager_cls 17 | 18 | class ManyRelatedManager(ManyRelatedManagerBase): 19 | 20 | def _add_items(self, source_field_name, target_field_name, *objs): 21 | """ 22 | By default, auto_created through objects from form instances are saved using 23 | Manager.bulk_create(). Manager.bulk_create() is passed a list containing 24 | instances of the through model with the target and source foreign keys defined. 25 | 26 | In order to set the position field we need to tweak this logic (the modified 27 | lines are marked out with comments below). 28 | 29 | This method is added to ManyRelatedManager below in 30 | SortableDescriptorMixin.related_manager_cls 31 | """ 32 | # source_field_name: the PK fieldname in join table for the source object 33 | # target_field_name: the PK fieldname in join table for the target object 34 | # *objs - objects to add. Either object instances, or primary keys of object instances. 35 | 36 | # If there aren't any objects, there is nothing to do. 37 | from django.db.models import Model 38 | if objs: 39 | new_ids = set() 40 | for obj in objs: 41 | if isinstance(obj, self.model): 42 | if not router.allow_relation(obj, self.instance): 43 | raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % 44 | (obj, self.instance._state.db, obj._state.db)) 45 | # _get_fk_val wasn't introduced until django 1.4.2 46 | if hasattr(self, '_get_fk_val'): 47 | fk_val = self._get_fk_val(obj, target_field_name) 48 | else: 49 | fk_val = obj.pk 50 | if fk_val is None: 51 | raise ValueError('Cannot add "%r": the value for field "%s" is None' % 52 | (obj, target_field_name)) 53 | new_ids.add(fk_val) 54 | elif isinstance(obj, Model): 55 | raise TypeError("'%s' instance expected, got %r" % (self.model._meta.object_name, obj)) 56 | else: 57 | new_ids.add(obj) 58 | db = router.db_for_write(self.through, instance=self.instance) 59 | vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True) 60 | vals = vals.filter(**{ 61 | source_field_name: getattr(self, '_pk_val', getattr(self, '_fk_val', self.instance.pk)), 62 | '%s__in' % target_field_name: new_ids, 63 | }) 64 | new_ids = new_ids - set(vals) 65 | 66 | if self.reverse or source_field_name == self.source_field_name: 67 | # Don't send the signal when we are inserting the 68 | # duplicate data row for symmetrical reverse entries. 69 | signals.m2m_changed.send(sender=self.through, action='pre_add', 70 | instance=self.instance, reverse=self.reverse, 71 | model=self.model, pk_set=new_ids, using=db) 72 | 73 | ###################################################################### 74 | # This is where we modify the default logic for _add_items(). 75 | # We use get_or_create for ALL objects. Typically it calls bulk_create 76 | # ONLY on ids which have not yet been created. 77 | ###################################################################### 78 | # sort_field = self.field.sort_field 79 | sort_field_attname = self.field.sort_field.attname 80 | for obj in objs: 81 | sort_position = getattr(obj, sort_field_attname) 82 | new_obj, created = self.through._default_manager.using(db).get_or_create(**{ 83 | sort_field_attname: sort_position, 84 | '%s_id' % source_field_name: getattr(self, '_pk_val', getattr(self, '_fk_val', self.instance.pk)), 85 | '%s_id' % target_field_name: obj.pk, 86 | }) 87 | if getattr(new_obj, sort_field_attname) is not sort_position: 88 | setattr(new_obj, sort_field_attname, sort_position) 89 | new_obj.save() 90 | ###################################################################### 91 | # End custom logic 92 | ###################################################################### 93 | 94 | if self.reverse or source_field_name == self.source_field_name: 95 | # Don't send the signal when we are inserting the 96 | # duplicate data row for symmetrical reverse entries. 97 | signals.m2m_changed.send(sender=self.through, action='post_add', 98 | instance=self.instance, reverse=self.reverse, 99 | model=self.model, pk_set=new_ids, using=db) 100 | 101 | def get_queryset(self): 102 | """ 103 | Adds ordering to ManyRelatedManager.get_queryset(). This is 104 | necessary in order for form widgets to display authors ordered by 105 | position. 106 | """ 107 | try: 108 | return self.instance._prefetched_objects_cache[self.prefetch_cache_name] 109 | except (AttributeError, KeyError): 110 | if django.VERSION < (1, 7): 111 | qset = super(ManyRelatedManager, self).get_query_set() 112 | else: 113 | qset = super(ManyRelatedManager, self).get_queryset() 114 | opts = self.through._meta 115 | # If the through table has Meta.ordering defined, order the objects 116 | # returned by the ManyRelatedManager by those fields. 117 | if self.field.sort_value_field_name: 118 | object_name = opts.object_name.lower() 119 | order_by = ['%s__%s' % (object_name, self.field.sort_value_field_name)] 120 | if self.model._meta.ordering != order_by: 121 | return qset.order_by(*order_by) 122 | return qset 123 | 124 | if django.VERSION < (1, 7): 125 | get_query_set = get_queryset 126 | 127 | def get_prefetch_queryset(self, instances, *args): 128 | if django.VERSION < (1, 7): 129 | rel_qs, rel_obj_attr, instance_attr, single, cache_name = \ 130 | super(ManyRelatedManager, self).get_prefetch_query_set(instances, *args) 131 | else: 132 | rel_qs, rel_obj_attr, instance_attr, single, cache_name = \ 133 | super(ManyRelatedManager, self).get_prefetch_queryset(instances, *args) 134 | opts = self.through._meta 135 | # If the through table has Meta.ordering defined, order the objects 136 | # returned by the ManyRelatedManager by those fields. 137 | if self.field.sort_value_field_name: 138 | object_name = opts.object_name.lower() 139 | order_by = ['%s__%s' % (object_name, self.field.sort_value_field_name)] 140 | if self.model._meta.ordering != order_by: 141 | rel_qs = rel_qs.order_by(*order_by) 142 | return (rel_qs, rel_obj_attr, instance_attr, single, cache_name) 143 | 144 | if django.VERSION < (1, 7): 145 | get_prefetch_query_set = get_prefetch_queryset 146 | 147 | ManyRelatedManager.field = self.field 148 | return ManyRelatedManager 149 | -------------------------------------------------------------------------------- /select2/static/select2/css/select2-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theatlantic/django-select2-forms/afa332e6848a3ff2a5df3eb461969815faefb723/select2/static/select2/css/select2-spinner.gif -------------------------------------------------------------------------------- /select2/static/select2/css/select2.css: -------------------------------------------------------------------------------- 1 | /* 2 | Version: 3.4.0 Timestamp: Fri Jun 21 17:46:55 EDT 2013 3 | */ 4 | .select2-container { 5 | margin: 0; 6 | position: relative; 7 | display: inline-block; 8 | /* inline-block for ie7 */ 9 | zoom: 1; 10 | *display: inline; 11 | vertical-align: middle; 12 | } 13 | 14 | .select2-container, 15 | .select2-drop, 16 | .select2-search, 17 | .select2-search input{ 18 | /* 19 | Force border-box so that % widths fit the parent 20 | container without overlap because of margin/padding. 21 | 22 | More Info : http://www.quirksmode.org/css/box.html 23 | */ 24 | -webkit-box-sizing: border-box; /* webkit */ 25 | -khtml-box-sizing: border-box; /* konqueror */ 26 | -moz-box-sizing: border-box; /* firefox */ 27 | -ms-box-sizing: border-box; /* ie */ 28 | box-sizing: border-box; /* css3 */ 29 | } 30 | 31 | .select2-container .select2-choice { 32 | display: block; 33 | height: 26px; 34 | padding: 0 0 0 8px; 35 | overflow: hidden; 36 | position: relative; 37 | 38 | border: 1px solid #aaa; 39 | white-space: nowrap; 40 | line-height: 26px; 41 | color: #444; 42 | text-decoration: none; 43 | 44 | -webkit-border-radius: 4px; 45 | -moz-border-radius: 4px; 46 | border-radius: 4px; 47 | 48 | -webkit-background-clip: padding-box; 49 | -moz-background-clip: padding; 50 | background-clip: padding-box; 51 | 52 | -webkit-touch-callout: none; 53 | -webkit-user-select: none; 54 | -khtml-user-select: none; 55 | -moz-user-select: none; 56 | -ms-user-select: none; 57 | user-select: none; 58 | 59 | background-color: #fff; 60 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.5, white)); 61 | background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 50%); 62 | background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 50%); 63 | background-image: -o-linear-gradient(bottom, #eeeeee 0%, #ffffff 50%); 64 | background-image: -ms-linear-gradient(top, #ffffff 0%, #eeeeee 50%); 65 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0); 66 | background-image: linear-gradient(top, #ffffff 0%, #eeeeee 50%); 67 | } 68 | 69 | .select2-container.select2-drop-above .select2-choice { 70 | border-bottom-color: #aaa; 71 | 72 | -webkit-border-radius:0 0 4px 4px; 73 | -moz-border-radius:0 0 4px 4px; 74 | border-radius:0 0 4px 4px; 75 | 76 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.9, white)); 77 | background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 90%); 78 | background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 90%); 79 | background-image: -o-linear-gradient(bottom, #eeeeee 0%, white 90%); 80 | background-image: -ms-linear-gradient(top, #eeeeee 0%,#ffffff 90%); 81 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 ); 82 | background-image: linear-gradient(top, #eeeeee 0%,#ffffff 90%); 83 | } 84 | 85 | .select2-container.select2-allowclear .select2-choice span { 86 | margin-right: 42px; 87 | } 88 | 89 | .select2-container .select2-choice span { 90 | margin-right: 26px; 91 | display: block; 92 | overflow: hidden; 93 | 94 | white-space: nowrap; 95 | 96 | -ms-text-overflow: ellipsis; 97 | -o-text-overflow: ellipsis; 98 | text-overflow: ellipsis; 99 | } 100 | 101 | .select2-container .select2-choice abbr { 102 | display: none; 103 | width: 12px; 104 | height: 12px; 105 | position: absolute; 106 | right: 24px; 107 | top: 8px; 108 | 109 | font-size: 1px; 110 | text-decoration: none; 111 | 112 | border: 0; 113 | background: url('select2.png') right top no-repeat; 114 | cursor: pointer; 115 | outline: 0; 116 | } 117 | 118 | .select2-container.select2-allowclear .select2-choice abbr { 119 | display: inline-block; 120 | } 121 | 122 | .select2-container .select2-choice abbr:hover { 123 | background-position: right -11px; 124 | cursor: pointer; 125 | } 126 | 127 | .select2-drop-undermask { 128 | border: 0; 129 | margin: 0; 130 | padding: 0; 131 | position: absolute; 132 | left: 0; 133 | top: 0; 134 | z-index: 9998; 135 | background-color: transparent; 136 | filter: alpha(opacity=0); 137 | } 138 | 139 | .select2-drop-mask { 140 | border: 0; 141 | margin: 0; 142 | padding: 0; 143 | position: absolute; 144 | left: 0; 145 | top: 0; 146 | z-index: 9998; 147 | /* styles required for IE to work */ 148 | background-color: #fff; 149 | opacity: 0; 150 | filter: alpha(opacity=0); 151 | } 152 | 153 | .select2-drop { 154 | width: 100%; 155 | margin-top:-1px; 156 | position: absolute; 157 | z-index: 9999; 158 | top: 100%; 159 | 160 | background: #fff; 161 | color: #000; 162 | border: 1px solid #aaa; 163 | border-top: 0; 164 | 165 | -webkit-border-radius: 0 0 4px 4px; 166 | -moz-border-radius: 0 0 4px 4px; 167 | border-radius: 0 0 4px 4px; 168 | 169 | -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); 170 | -moz-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); 171 | box-shadow: 0 4px 5px rgba(0, 0, 0, .15); 172 | } 173 | 174 | .select2-drop-auto-width { 175 | border-top: 1px solid #aaa; 176 | width: auto; 177 | } 178 | 179 | .select2-drop-auto-width .select2-search { 180 | padding-top: 4px; 181 | } 182 | 183 | .select2-drop.select2-drop-above { 184 | margin-top: 1px; 185 | border-top: 1px solid #aaa; 186 | border-bottom: 0; 187 | 188 | -webkit-border-radius: 4px 4px 0 0; 189 | -moz-border-radius: 4px 4px 0 0; 190 | border-radius: 4px 4px 0 0; 191 | 192 | -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); 193 | -moz-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); 194 | box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); 195 | } 196 | 197 | .select2-drop-active { 198 | border: 1px solid #5897fb; 199 | border-top: none; 200 | } 201 | 202 | .select2-drop.select2-drop-above.select2-drop-active { 203 | border-top: 1px solid #5897fb; 204 | } 205 | 206 | .select2-container .select2-choice div { 207 | display: inline-block; 208 | width: 18px; 209 | height: 100%; 210 | position: absolute; 211 | right: 0; 212 | top: 0; 213 | 214 | border-left: 1px solid #aaa; 215 | -webkit-border-radius: 0 4px 4px 0; 216 | -moz-border-radius: 0 4px 4px 0; 217 | border-radius: 0 4px 4px 0; 218 | 219 | -webkit-background-clip: padding-box; 220 | -moz-background-clip: padding; 221 | background-clip: padding-box; 222 | 223 | background: #ccc; 224 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee)); 225 | background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%); 226 | background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%); 227 | background-image: -o-linear-gradient(bottom, #ccc 0%, #eee 60%); 228 | background-image: -ms-linear-gradient(top, #cccccc 0%, #eeeeee 60%); 229 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#cccccc', GradientType = 0); 230 | background-image: linear-gradient(top, #cccccc 0%, #eeeeee 60%); 231 | } 232 | 233 | .select2-container .select2-choice div b { 234 | display: block; 235 | width: 100%; 236 | height: 100%; 237 | background: url('select2.png') no-repeat 0 1px; 238 | } 239 | 240 | .select2-search { 241 | display: inline-block; 242 | width: 100%; 243 | min-height: 26px; 244 | margin: 0; 245 | padding-left: 4px; 246 | padding-right: 4px; 247 | 248 | position: relative; 249 | z-index: 10000; 250 | 251 | white-space: nowrap; 252 | } 253 | 254 | .select2-search input { 255 | width: 100%; 256 | height: auto !important; 257 | min-height: 26px; 258 | padding: 4px 20px 4px 5px; 259 | margin: 0; 260 | 261 | outline: 0; 262 | font-family: sans-serif; 263 | font-size: 1em; 264 | 265 | border: 1px solid #aaa; 266 | -webkit-border-radius: 0; 267 | -moz-border-radius: 0; 268 | border-radius: 0; 269 | 270 | -webkit-box-shadow: none; 271 | -moz-box-shadow: none; 272 | box-shadow: none; 273 | 274 | background: #fff url('select2.png') no-repeat 100% -22px; 275 | background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); 276 | background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); 277 | background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); 278 | background: url('select2.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); 279 | background: url('select2.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%); 280 | background: url('select2.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%, #eeeeee 99%); 281 | } 282 | 283 | .select2-drop.select2-drop-above .select2-search input { 284 | margin-top: 4px; 285 | } 286 | 287 | .select2-search input.select2-active { 288 | background: #fff url('select2-spinner.gif') no-repeat 100%; 289 | background: url('select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); 290 | background: url('select2-spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); 291 | background: url('select2-spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); 292 | background: url('select2-spinner.gif') no-repeat 100%, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); 293 | background: url('select2-spinner.gif') no-repeat 100%, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%); 294 | background: url('select2-spinner.gif') no-repeat 100%, linear-gradient(top, #ffffff 85%, #eeeeee 99%); 295 | } 296 | 297 | .select2-container-active .select2-choice, 298 | .select2-container-active .select2-choices { 299 | border: 1px solid #5897fb; 300 | outline: none; 301 | 302 | -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); 303 | -moz-box-shadow: 0 0 5px rgba(0,0,0,.3); 304 | box-shadow: 0 0 5px rgba(0,0,0,.3); 305 | } 306 | 307 | .select2-dropdown-open .select2-choice { 308 | border-bottom-color: transparent; 309 | -webkit-box-shadow: 0 1px 0 #fff inset; 310 | -moz-box-shadow: 0 1px 0 #fff inset; 311 | box-shadow: 0 1px 0 #fff inset; 312 | 313 | -webkit-border-bottom-left-radius: 0; 314 | -moz-border-radius-bottomleft: 0; 315 | border-bottom-left-radius: 0; 316 | 317 | -webkit-border-bottom-right-radius: 0; 318 | -moz-border-radius-bottomright: 0; 319 | border-bottom-right-radius: 0; 320 | 321 | background-color: #eee; 322 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, white), color-stop(0.5, #eeeeee)); 323 | background-image: -webkit-linear-gradient(center bottom, white 0%, #eeeeee 50%); 324 | background-image: -moz-linear-gradient(center bottom, white 0%, #eeeeee 50%); 325 | background-image: -o-linear-gradient(bottom, white 0%, #eeeeee 50%); 326 | background-image: -ms-linear-gradient(top, #ffffff 0%,#eeeeee 50%); 327 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 ); 328 | background-image: linear-gradient(top, #ffffff 0%,#eeeeee 50%); 329 | } 330 | 331 | .select2-dropdown-open.select2-drop-above .select2-choice, 332 | .select2-dropdown-open.select2-drop-above .select2-choices { 333 | border: 1px solid #5897fb; 334 | border-top-color: transparent; 335 | 336 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, white), color-stop(0.5, #eeeeee)); 337 | background-image: -webkit-linear-gradient(center top, white 0%, #eeeeee 50%); 338 | background-image: -moz-linear-gradient(center top, white 0%, #eeeeee 50%); 339 | background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); 340 | background-image: -ms-linear-gradient(bottom, #ffffff 0%,#eeeeee 50%); 341 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 ); 342 | background-image: linear-gradient(bottom, #ffffff 0%,#eeeeee 50%); 343 | } 344 | 345 | .select2-dropdown-open .select2-choice div { 346 | background: transparent; 347 | border-left: none; 348 | filter: none; 349 | } 350 | .select2-dropdown-open .select2-choice div b { 351 | background-position: -18px 1px; 352 | } 353 | 354 | /* results */ 355 | .select2-results { 356 | max-height: 200px; 357 | padding: 0 0 0 4px; 358 | margin: 4px 4px 4px 0; 359 | position: relative; 360 | overflow-x: hidden; 361 | overflow-y: auto; 362 | -webkit-tap-highlight-color: rgba(0,0,0,0); 363 | } 364 | 365 | .select2-results ul.select2-result-sub { 366 | margin: 0; 367 | padding-left: 0; 368 | } 369 | 370 | .select2-results ul.select2-result-sub > li .select2-result-label { padding-left: 20px } 371 | .select2-results ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 40px } 372 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 60px } 373 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 80px } 374 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 100px } 375 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 110px } 376 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 120px } 377 | 378 | .select2-results li { 379 | list-style: none; 380 | display: list-item; 381 | background-image: none; 382 | } 383 | 384 | .select2-results li.select2-result-with-children > .select2-result-label { 385 | font-weight: bold; 386 | } 387 | 388 | .select2-results .select2-result-label { 389 | padding: 3px 7px 4px; 390 | margin: 0; 391 | cursor: pointer; 392 | 393 | min-height: 1em; 394 | 395 | -webkit-touch-callout: none; 396 | -webkit-user-select: none; 397 | -khtml-user-select: none; 398 | -moz-user-select: none; 399 | -ms-user-select: none; 400 | user-select: none; 401 | } 402 | 403 | .select2-results .select2-highlighted { 404 | background: #3875d7; 405 | color: #fff; 406 | } 407 | 408 | .select2-results li em { 409 | background: #feffde; 410 | font-style: normal; 411 | } 412 | 413 | .select2-results .select2-highlighted em { 414 | background: transparent; 415 | } 416 | 417 | .select2-results .select2-highlighted ul { 418 | background: white; 419 | color: #000; 420 | } 421 | 422 | 423 | .select2-results .select2-no-results, 424 | .select2-results .select2-searching, 425 | .select2-results .select2-selection-limit { 426 | background: #f4f4f4; 427 | display: list-item; 428 | } 429 | 430 | /* 431 | disabled look for disabled choices in the results dropdown 432 | */ 433 | .select2-results .select2-disabled.select2-highlighted { 434 | color: #666; 435 | background: #f4f4f4; 436 | display: list-item; 437 | cursor: default; 438 | } 439 | .select2-results .select2-disabled { 440 | background: #f4f4f4; 441 | display: list-item; 442 | cursor: default; 443 | } 444 | 445 | .select2-results .select2-selected { 446 | display: none; 447 | } 448 | 449 | .select2-more-results.select2-active { 450 | background: #f4f4f4 url('select2-spinner.gif') no-repeat 100%; 451 | } 452 | 453 | .select2-more-results { 454 | background: #f4f4f4; 455 | display: list-item; 456 | } 457 | 458 | /* disabled styles */ 459 | 460 | .select2-container.select2-container-disabled .select2-choice { 461 | background-color: #f4f4f4; 462 | background-image: none; 463 | border: 1px solid #ddd; 464 | cursor: default; 465 | } 466 | 467 | .select2-container.select2-container-disabled .select2-choice div { 468 | background-color: #f4f4f4; 469 | background-image: none; 470 | border-left: 0; 471 | } 472 | 473 | .select2-container.select2-container-disabled .select2-choice abbr { 474 | display: none; 475 | } 476 | 477 | 478 | /* multiselect */ 479 | 480 | .select2-container-multi .select2-choices { 481 | height: auto !important; 482 | height: 1%; 483 | margin: 0; 484 | padding: 0; 485 | position: relative; 486 | 487 | border: 1px solid #aaa; 488 | cursor: text; 489 | overflow: hidden; 490 | 491 | background-color: #fff; 492 | background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff)); 493 | background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 494 | background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 495 | background-image: -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 496 | background-image: -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 497 | background-image: linear-gradient(top, #eeeeee 1%, #ffffff 15%); 498 | } 499 | 500 | .select2-locked { 501 | padding: 3px 5px 3px 5px !important; 502 | } 503 | 504 | .select2-container-multi .select2-choices { 505 | min-height: 26px; 506 | } 507 | 508 | .select2-container-multi.select2-container-active .select2-choices { 509 | border: 1px solid #5897fb; 510 | outline: none; 511 | 512 | -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); 513 | -moz-box-shadow: 0 0 5px rgba(0,0,0,.3); 514 | box-shadow: 0 0 5px rgba(0,0,0,.3); 515 | } 516 | .select2-container-multi .select2-choices li { 517 | float: left; 518 | list-style: none; 519 | } 520 | .select2-container-multi .select2-choices .select2-search-field { 521 | margin: 0; 522 | padding: 0; 523 | white-space: nowrap; 524 | } 525 | 526 | .select2-container-multi .select2-choices .select2-search-field input { 527 | padding: 5px; 528 | margin: 1px 0; 529 | 530 | font-family: sans-serif; 531 | font-size: 100%; 532 | color: #666; 533 | outline: 0; 534 | border: 0; 535 | -webkit-box-shadow: none; 536 | -moz-box-shadow: none; 537 | box-shadow: none; 538 | background: transparent !important; 539 | } 540 | 541 | .select2-container-multi .select2-choices .select2-search-field input.select2-active { 542 | background: #fff url('select2-spinner.gif') no-repeat 100% !important; 543 | } 544 | 545 | .select2-default { 546 | color: #999 !important; 547 | } 548 | 549 | .select2-container-multi .select2-choices .select2-search-choice { 550 | padding: 3px 5px 3px 18px; 551 | margin: 3px 0 3px 5px; 552 | position: relative; 553 | 554 | line-height: 13px; 555 | color: #333; 556 | cursor: default; 557 | border: 1px solid #aaaaaa; 558 | 559 | -webkit-border-radius: 3px; 560 | -moz-border-radius: 3px; 561 | border-radius: 3px; 562 | 563 | -webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); 564 | -moz-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); 565 | box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); 566 | 567 | -webkit-background-clip: padding-box; 568 | -moz-background-clip: padding; 569 | background-clip: padding-box; 570 | 571 | -webkit-touch-callout: none; 572 | -webkit-user-select: none; 573 | -khtml-user-select: none; 574 | -moz-user-select: none; 575 | -ms-user-select: none; 576 | user-select: none; 577 | 578 | background-color: #e4e4e4; 579 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0 ); 580 | background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee)); 581 | background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 582 | background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 583 | background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 584 | background-image: -ms-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 585 | background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 586 | } 587 | .select2-container-multi .select2-choices .select2-search-choice span { 588 | cursor: default; 589 | } 590 | .select2-container-multi .select2-choices .select2-search-choice-focus { 591 | background: #d4d4d4; 592 | } 593 | 594 | .select2-search-choice-close { 595 | display: block; 596 | width: 12px; 597 | height: 13px; 598 | position: absolute; 599 | right: 3px; 600 | top: 4px; 601 | 602 | font-size: 1px; 603 | outline: none; 604 | background: url('select2.png') right top no-repeat; 605 | } 606 | 607 | .select2-container-multi .select2-search-choice-close { 608 | left: 3px; 609 | } 610 | 611 | .select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover { 612 | background-position: right -11px; 613 | } 614 | .select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close { 615 | background-position: right -11px; 616 | } 617 | 618 | /* disabled styles */ 619 | .select2-container-multi.select2-container-disabled .select2-choices{ 620 | background-color: #f4f4f4; 621 | background-image: none; 622 | border: 1px solid #ddd; 623 | cursor: default; 624 | } 625 | 626 | .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice { 627 | padding: 3px 5px 3px 5px; 628 | border: 1px solid #ddd; 629 | background-image: none; 630 | background-color: #f4f4f4; 631 | } 632 | 633 | .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { display: none; 634 | background:none; 635 | } 636 | /* end multiselect */ 637 | 638 | 639 | .select2-result-selectable .select2-match, 640 | .select2-result-unselectable .select2-match { 641 | text-decoration: underline; 642 | } 643 | 644 | .select2-offscreen, .select2-offscreen:focus { 645 | clip: rect(0 0 0 0); 646 | width: 1px; 647 | height: 1px; 648 | border: 0; 649 | margin: 0; 650 | padding: 0; 651 | overflow: hidden; 652 | position: absolute; 653 | outline: 0; 654 | left: 0px; 655 | } 656 | 657 | .select2-display-none { 658 | display: none; 659 | } 660 | 661 | .select2-measure-scrollbar { 662 | position: absolute; 663 | top: -10000px; 664 | left: -10000px; 665 | width: 100px; 666 | height: 100px; 667 | overflow: scroll; 668 | } 669 | /* Retina-ize icons */ 670 | 671 | @media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi) { 672 | .select2-search input, .select2-search-choice-close, .select2-container .select2-choice abbr, .select2-container .select2-choice div b { 673 | background-image: url('select2x2.png') !important; 674 | background-repeat: no-repeat !important; 675 | background-size: 60px 40px !important; 676 | } 677 | .select2-search input { 678 | background-position: 100% -21px !important; 679 | } 680 | } 681 | -------------------------------------------------------------------------------- /select2/static/select2/css/select2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theatlantic/django-select2-forms/afa332e6848a3ff2a5df3eb461969815faefb723/select2/static/select2/css/select2.png -------------------------------------------------------------------------------- /select2/static/select2/css/select2x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theatlantic/django-select2-forms/afa332e6848a3ff2a5df3eb461969815faefb723/select2/static/select2/css/select2x2.png -------------------------------------------------------------------------------- /select2/static/select2/js/select2.jquery_ready.js: -------------------------------------------------------------------------------- 1 | var DjangoSelect2 = window.DjangoSelect2 || {}; 2 | 3 | (function() { 4 | var $ = DjangoSelect2.jQuery; 5 | if (!$) { 6 | $ = DjangoSelect2.jQuery = (window.django || {}).jQuery || window.jQuery; 7 | } 8 | 9 | DjangoSelect2.init = function(input) { 10 | var $input = $(input); 11 | var ajaxOptions = { 12 | ajax: { 13 | data: function(term, page) { 14 | return { 15 | q: term, 16 | page: page, 17 | page_limit: 10 18 | }; 19 | }, 20 | results: function(data, page) { 21 | return data; 22 | } 23 | }, 24 | initSelection: function (element, callback) { 25 | var inputVal = $input.val(); 26 | if (!inputVal) { 27 | return; 28 | } 29 | var data = { 30 | q: inputVal 31 | }; 32 | var select2 = $input.data('select2') || {}; 33 | if (typeof select2.opts == 'object' && select2.opts !== null) { 34 | data.multiple = (select2.opts.multiple) ? 1 : 0; 35 | } 36 | $.ajax({ 37 | url: $input.data('initSelectionUrl'), 38 | dataType: 'json', 39 | data: data, 40 | success: function(data) { 41 | if (typeof(data) == 'object' && typeof(data.results) == 'object' && data.results) { 42 | callback(data.results); 43 | } 44 | } 45 | }); 46 | } 47 | }; 48 | var options = $input.data('select2Options') || {}; 49 | if (typeof options.ajax == 'object') { 50 | options = $.extend(true, ajaxOptions, options); 51 | } 52 | $input.select2(options); 53 | if ($input.data('sortable') && typeof $.fn.djs2sortable == 'function') { 54 | $input.select2("container").find("ul.select2-choices").djs2sortable({ 55 | containment: 'parent', 56 | start: function() { $input.select2("onSortStart"); }, 57 | update: function() { $input.select2("onSortEnd"); } 58 | }); 59 | } 60 | $input.data('select2').selection.addClass('vTextField'); 61 | }; 62 | 63 | $(document).ready(function() { 64 | $('.django-select2:not([name*="__prefix__"])').each(function() { 65 | DjangoSelect2.init(this); 66 | }); 67 | $(document).on('formset:added', function(event, $form) { 68 | $form.find('.django-select2:not([name*="__prefix__"])').each(function() { 69 | DjangoSelect2.init(this); 70 | }); 71 | }); 72 | }); 73 | })(); 74 | -------------------------------------------------------------------------------- /select2/static/select2/js/select2.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Igor Vaynberg 3 | 4 | Version: 3.4.0 Timestamp: Fri Jun 21 17:46:55 EDT 2013 5 | 6 | This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU 7 | General Public License version 2 (the "GPL License"). You may choose either license to govern your 8 | use of this software only upon the condition that you accept all of the terms of either the Apache 9 | License or the GPL License. 10 | 11 | You may obtain a copy of the Apache License and the GPL License at: 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | http://www.gnu.org/licenses/gpl-2.0.html 15 | 16 | Unless required by applicable law or agreed to in writing, software distributed under the Apache License 17 | or the GPL Licesnse is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 18 | either express or implied. See the Apache License and the GPL License for the specific language governing 19 | permissions and limitations under the Apache License and the GPL License. 20 | 21 | LICENSE #1: Apache License, Version 2.0 22 | 23 | LICENSE #2: GPL v2 24 | 25 | ************************************************************ 26 | 27 | LICENSE #1: 28 | 29 | Apache License 30 | Version 2.0, January 2004 31 | http://www.apache.org/licenses/ 32 | 33 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 34 | 35 | 1. Definitions. 36 | 37 | "License" shall mean the terms and conditions for use, reproduction, 38 | and distribution as defined by Sections 1 through 9 of this document. 39 | 40 | "Licensor" shall mean the copyright owner or entity authorized by 41 | the copyright owner that is granting the License. 42 | 43 | "Legal Entity" shall mean the union of the acting entity and all 44 | other entities that control, are controlled by, or are under common 45 | control with that entity. For the purposes of this definition, 46 | "control" means (i) the power, direct or indirect, to cause the 47 | direction or management of such entity, whether by contract or 48 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 49 | outstanding shares, or (iii) beneficial ownership of such entity. 50 | 51 | "You" (or "Your") shall mean an individual or Legal Entity 52 | exercising permissions granted by this License. 53 | 54 | "Source" form shall mean the preferred form for making modifications, 55 | including but not limited to software source code, documentation 56 | source, and configuration files. 57 | 58 | "Object" form shall mean any form resulting from mechanical 59 | transformation or translation of a Source form, including but 60 | not limited to compiled object code, generated documentation, 61 | and conversions to other media types. 62 | 63 | "Work" shall mean the work of authorship, whether in Source or 64 | Object form, made available under the License, as indicated by a 65 | copyright notice that is included in or attached to the work 66 | (an example is provided in the Appendix below). 67 | 68 | "Derivative Works" shall mean any work, whether in Source or Object 69 | form, that is based on (or derived from) the Work and for which the 70 | editorial revisions, annotations, elaborations, or other modifications 71 | represent, as a whole, an original work of authorship. For the purposes 72 | of this License, Derivative Works shall not include works that remain 73 | separable from, or merely link (or bind by name) to the interfaces of, 74 | the Work and Derivative Works thereof. 75 | 76 | "Contribution" shall mean any work of authorship, including 77 | the original version of the Work and any modifications or additions 78 | to that Work or Derivative Works thereof, that is intentionally 79 | submitted to Licensor for inclusion in the Work by the copyright owner 80 | or by an individual or Legal Entity authorized to submit on behalf of 81 | the copyright owner. For the purposes of this definition, "submitted" 82 | means any form of electronic, verbal, or written communication sent 83 | to the Licensor or its representatives, including but not limited to 84 | communication on electronic mailing lists, source code control systems, 85 | and issue tracking systems that are managed by, or on behalf of, the 86 | Licensor for the purpose of discussing and improving the Work, but 87 | excluding communication that is conspicuously marked or otherwise 88 | designated in writing by the copyright owner as "Not a Contribution." 89 | 90 | "Contributor" shall mean Licensor and any individual or Legal Entity 91 | on behalf of whom a Contribution has been received by Licensor and 92 | subsequently incorporated within the Work. 93 | 94 | 2. Grant of Copyright License. Subject to the terms and conditions of 95 | this License, each Contributor hereby grants to You a perpetual, 96 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 97 | copyright license to reproduce, prepare Derivative Works of, 98 | publicly display, publicly perform, sublicense, and distribute the 99 | Work and such Derivative Works in Source or Object form. 100 | 101 | 3. Grant of Patent License. Subject to the terms and conditions of 102 | this License, each Contributor hereby grants to You a perpetual, 103 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 104 | (except as stated in this section) patent license to make, have made, 105 | use, offer to sell, sell, import, and otherwise transfer the Work, 106 | where such license applies only to those patent claims licensable 107 | by such Contributor that are necessarily infringed by their 108 | Contribution(s) alone or by combination of their Contribution(s) 109 | with the Work to which such Contribution(s) was submitted. If You 110 | institute patent litigation against any entity (including a 111 | cross-claim or counterclaim in a lawsuit) alleging that the Work 112 | or a Contribution incorporated within the Work constitutes direct 113 | or contributory patent infringement, then any patent licenses 114 | granted to You under this License for that Work shall terminate 115 | as of the date such litigation is filed. 116 | 117 | 4. Redistribution. You may reproduce and distribute copies of the 118 | Work or Derivative Works thereof in any medium, with or without 119 | modifications, and in Source or Object form, provided that You 120 | meet the following conditions: 121 | 122 | (a) You must give any other recipients of the Work or 123 | Derivative Works a copy of this License; and 124 | 125 | (b) You must cause any modified files to carry prominent notices 126 | stating that You changed the files; and 127 | 128 | (c) You must retain, in the Source form of any Derivative Works 129 | that You distribute, all copyright, patent, trademark, and 130 | attribution notices from the Source form of the Work, 131 | excluding those notices that do not pertain to any part of 132 | the Derivative Works; and 133 | 134 | (d) If the Work includes a "NOTICE" text file as part of its 135 | distribution, then any Derivative Works that You distribute must 136 | include a readable copy of the attribution notices contained 137 | within such NOTICE file, excluding those notices that do not 138 | pertain to any part of the Derivative Works, in at least one 139 | of the following places: within a NOTICE text file distributed 140 | as part of the Derivative Works; within the Source form or 141 | documentation, if provided along with the Derivative Works; or, 142 | within a display generated by the Derivative Works, if and 143 | wherever such third-party notices normally appear. The contents 144 | of the NOTICE file are for informational purposes only and 145 | do not modify the License. You may add Your own attribution 146 | notices within Derivative Works that You distribute, alongside 147 | or as an addendum to the NOTICE text from the Work, provided 148 | that such additional attribution notices cannot be construed 149 | as modifying the License. 150 | 151 | You may add Your own copyright statement to Your modifications and 152 | may provide additional or different license terms and conditions 153 | for use, reproduction, or distribution of Your modifications, or 154 | for any such Derivative Works as a whole, provided Your use, 155 | reproduction, and distribution of the Work otherwise complies with 156 | the conditions stated in this License. 157 | 158 | 5. Submission of Contributions. Unless You explicitly state otherwise, 159 | any Contribution intentionally submitted for inclusion in the Work 160 | by You to the Licensor shall be under the terms and conditions of 161 | this License, without any additional terms or conditions. 162 | Notwithstanding the above, nothing herein shall supersede or modify 163 | the terms of any separate license agreement you may have executed 164 | with Licensor regarding such Contributions. 165 | 166 | 6. Trademarks. This License does not grant permission to use the trade 167 | names, trademarks, service marks, or product names of the Licensor, 168 | except as required for reasonable and customary use in describing the 169 | origin of the Work and reproducing the content of the NOTICE file. 170 | 171 | 7. Disclaimer of Warranty. Unless required by applicable law or 172 | agreed to in writing, Licensor provides the Work (and each 173 | Contributor provides its Contributions) on an "AS IS" BASIS, 174 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 175 | implied, including, without limitation, any warranties or conditions 176 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 177 | PARTICULAR PURPOSE. You are solely responsible for determining the 178 | appropriateness of using or redistributing the Work and assume any 179 | risks associated with Your exercise of permissions under this License. 180 | 181 | 8. Limitation of Liability. In no event and under no legal theory, 182 | whether in tort (including negligence), contract, or otherwise, 183 | unless required by applicable law (such as deliberate and grossly 184 | negligent acts) or agreed to in writing, shall any Contributor be 185 | liable to You for damages, including any direct, indirect, special, 186 | incidental, or consequential damages of any character arising as a 187 | result of this License or out of the use or inability to use the 188 | Work (including but not limited to damages for loss of goodwill, 189 | work stoppage, computer failure or malfunction, or any and all 190 | other commercial damages or losses), even if such Contributor 191 | has been advised of the possibility of such damages. 192 | 193 | 9. Accepting Warranty or Additional Liability. While redistributing 194 | the Work or Derivative Works thereof, You may choose to offer, 195 | and charge a fee for, acceptance of support, warranty, indemnity, 196 | or other liability obligations and/or rights consistent with this 197 | License. However, in accepting such obligations, You may act only 198 | on Your own behalf and on Your sole responsibility, not on behalf 199 | of any other Contributor, and only if You agree to indemnify, 200 | defend, and hold each Contributor harmless for any liability 201 | incurred by, or claims asserted against, such Contributor by reason 202 | of your accepting any such warranty or additional liability. 203 | 204 | ************************************************************ 205 | 206 | LICENSE #2: 207 | 208 | GNU GENERAL PUBLIC LICENSE 209 | Version 2, June 1991 210 | 211 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 212 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 213 | Everyone is permitted to copy and distribute verbatim copies 214 | of this license document, but changing it is not allowed. 215 | 216 | Preamble 217 | 218 | The licenses for most software are designed to take away your 219 | freedom to share and change it. By contrast, the GNU General Public 220 | License is intended to guarantee your freedom to share and change free 221 | software--to make sure the software is free for all its users. This 222 | General Public License applies to most of the Free Software 223 | Foundation's software and to any other program whose authors commit to 224 | using it. (Some other Free Software Foundation software is covered by 225 | the GNU Lesser General Public License instead.) You can apply it to 226 | your programs, too. 227 | 228 | When we speak of free software, we are referring to freedom, not 229 | price. Our General Public Licenses are designed to make sure that you 230 | have the freedom to distribute copies of free software (and charge for 231 | this service if you wish), that you receive source code or can get it 232 | if you want it, that you can change the software or use pieces of it 233 | in new free programs; and that you know you can do these things. 234 | 235 | To protect your rights, we need to make restrictions that forbid 236 | anyone to deny you these rights or to ask you to surrender the rights. 237 | These restrictions translate to certain responsibilities for you if you 238 | distribute copies of the software, or if you modify it. 239 | 240 | For example, if you distribute copies of such a program, whether 241 | gratis or for a fee, you must give the recipients all the rights that 242 | you have. You must make sure that they, too, receive or can get the 243 | source code. And you must show them these terms so they know their 244 | rights. 245 | 246 | We protect your rights with two steps: (1) copyright the software, and 247 | (2) offer you this license which gives you legal permission to copy, 248 | distribute and/or modify the software. 249 | 250 | Also, for each author's protection and ours, we want to make certain 251 | that everyone understands that there is no warranty for this free 252 | software. If the software is modified by someone else and passed on, we 253 | want its recipients to know that what they have is not the original, so 254 | that any problems introduced by others will not reflect on the original 255 | authors' reputations. 256 | 257 | Finally, any free program is threatened constantly by software 258 | patents. We wish to avoid the danger that redistributors of a free 259 | program will individually obtain patent licenses, in effect making the 260 | program proprietary. To prevent this, we have made it clear that any 261 | patent must be licensed for everyone's free use or not licensed at all. 262 | 263 | The precise terms and conditions for copying, distribution and 264 | modification follow. 265 | 266 | GNU GENERAL PUBLIC LICENSE 267 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 268 | 269 | 0. This License applies to any program or other work which contains 270 | a notice placed by the copyright holder saying it may be distributed 271 | under the terms of this General Public License. The "Program", below, 272 | refers to any such program or work, and a "work based on the Program" 273 | means either the Program or any derivative work under copyright law: 274 | that is to say, a work containing the Program or a portion of it, 275 | either verbatim or with modifications and/or translated into another 276 | language. (Hereinafter, translation is included without limitation in 277 | the term "modification".) Each licensee is addressed as "you". 278 | 279 | Activities other than copying, distribution and modification are not 280 | covered by this License; they are outside its scope. The act of 281 | running the Program is not restricted, and the output from the Program 282 | is covered only if its contents constitute a work based on the 283 | Program (independent of having been made by running the Program). 284 | Whether that is true depends on what the Program does. 285 | 286 | 1. You may copy and distribute verbatim copies of the Program's 287 | source code as you receive it, in any medium, provided that you 288 | conspicuously and appropriately publish on each copy an appropriate 289 | copyright notice and disclaimer of warranty; keep intact all the 290 | notices that refer to this License and to the absence of any warranty; 291 | and give any other recipients of the Program a copy of this License 292 | along with the Program. 293 | 294 | You may charge a fee for the physical act of transferring a copy, and 295 | you may at your option offer warranty protection in exchange for a fee. 296 | 297 | 2. You may modify your copy or copies of the Program or any portion 298 | of it, thus forming a work based on the Program, and copy and 299 | distribute such modifications or work under the terms of Section 1 300 | above, provided that you also meet all of these conditions: 301 | 302 | a) You must cause the modified files to carry prominent notices 303 | stating that you changed the files and the date of any change. 304 | 305 | b) You must cause any work that you distribute or publish, that in 306 | whole or in part contains or is derived from the Program or any 307 | part thereof, to be licensed as a whole at no charge to all third 308 | parties under the terms of this License. 309 | 310 | c) If the modified program normally reads commands interactively 311 | when run, you must cause it, when started running for such 312 | interactive use in the most ordinary way, to print or display an 313 | announcement including an appropriate copyright notice and a 314 | notice that there is no warranty (or else, saying that you provide 315 | a warranty) and that users may redistribute the program under 316 | these conditions, and telling the user how to view a copy of this 317 | License. (Exception: if the Program itself is interactive but 318 | does not normally print such an announcement, your work based on 319 | the Program is not required to print an announcement.) 320 | 321 | These requirements apply to the modified work as a whole. If 322 | identifiable sections of that work are not derived from the Program, 323 | and can be reasonably considered independent and separate works in 324 | themselves, then this License, and its terms, do not apply to those 325 | sections when you distribute them as separate works. But when you 326 | distribute the same sections as part of a whole which is a work based 327 | on the Program, the distribution of the whole must be on the terms of 328 | this License, whose permissions for other licensees extend to the 329 | entire whole, and thus to each and every part regardless of who wrote it. 330 | 331 | Thus, it is not the intent of this section to claim rights or contest 332 | your rights to work written entirely by you; rather, the intent is to 333 | exercise the right to control the distribution of derivative or 334 | collective works based on the Program. 335 | 336 | In addition, mere aggregation of another work not based on the Program 337 | with the Program (or with a work based on the Program) on a volume of 338 | a storage or distribution medium does not bring the other work under 339 | the scope of this License. 340 | 341 | 3. You may copy and distribute the Program (or a work based on it, 342 | under Section 2) in object code or executable form under the terms of 343 | Sections 1 and 2 above provided that you also do one of the following: 344 | 345 | a) Accompany it with the complete corresponding machine-readable 346 | source code, which must be distributed under the terms of Sections 347 | 1 and 2 above on a medium customarily used for software interchange; or, 348 | 349 | b) Accompany it with a written offer, valid for at least three 350 | years, to give any third party, for a charge no more than your 351 | cost of physically performing source distribution, a complete 352 | machine-readable copy of the corresponding source code, to be 353 | distributed under the terms of Sections 1 and 2 above on a medium 354 | customarily used for software interchange; or, 355 | 356 | c) Accompany it with the information you received as to the offer 357 | to distribute corresponding source code. (This alternative is 358 | allowed only for noncommercial distribution and only if you 359 | received the program in object code or executable form with such 360 | an offer, in accord with Subsection b above.) 361 | 362 | The source code for a work means the preferred form of the work for 363 | making modifications to it. For an executable work, complete source 364 | code means all the source code for all modules it contains, plus any 365 | associated interface definition files, plus the scripts used to 366 | control compilation and installation of the executable. However, as a 367 | special exception, the source code distributed need not include 368 | anything that is normally distributed (in either source or binary 369 | form) with the major components (compiler, kernel, and so on) of the 370 | operating system on which the executable runs, unless that component 371 | itself accompanies the executable. 372 | 373 | If distribution of executable or object code is made by offering 374 | access to copy from a designated place, then offering equivalent 375 | access to copy the source code from the same place counts as 376 | distribution of the source code, even though third parties are not 377 | compelled to copy the source along with the object code. 378 | 379 | 4. You may not copy, modify, sublicense, or distribute the Program 380 | except as expressly provided under this License. Any attempt 381 | otherwise to copy, modify, sublicense or distribute the Program is 382 | void, and will automatically terminate your rights under this License. 383 | However, parties who have received copies, or rights, from you under 384 | this License will not have their licenses terminated so long as such 385 | parties remain in full compliance. 386 | 387 | 5. You are not required to accept this License, since you have not 388 | signed it. However, nothing else grants you permission to modify or 389 | distribute the Program or its derivative works. These actions are 390 | prohibited by law if you do not accept this License. Therefore, by 391 | modifying or distributing the Program (or any work based on the 392 | Program), you indicate your acceptance of this License to do so, and 393 | all its terms and conditions for copying, distributing or modifying 394 | the Program or works based on it. 395 | 396 | 6. Each time you redistribute the Program (or any work based on the 397 | Program), the recipient automatically receives a license from the 398 | original licensor to copy, distribute or modify the Program subject to 399 | these terms and conditions. You may not impose any further 400 | restrictions on the recipients' exercise of the rights granted herein. 401 | You are not responsible for enforcing compliance by third parties to 402 | this License. 403 | 404 | 7. If, as a consequence of a court judgment or allegation of patent 405 | infringement or for any other reason (not limited to patent issues), 406 | conditions are imposed on you (whether by court order, agreement or 407 | otherwise) that contradict the conditions of this License, they do not 408 | excuse you from the conditions of this License. If you cannot 409 | distribute so as to satisfy simultaneously your obligations under this 410 | License and any other pertinent obligations, then as a consequence you 411 | may not distribute the Program at all. For example, if a patent 412 | license would not permit royalty-free redistribution of the Program by 413 | all those who receive copies directly or indirectly through you, then 414 | the only way you could satisfy both it and this License would be to 415 | refrain entirely from distribution of the Program. 416 | 417 | If any portion of this section is held invalid or unenforceable under 418 | any particular circumstance, the balance of the section is intended to 419 | apply and the section as a whole is intended to apply in other 420 | circumstances. 421 | 422 | It is not the purpose of this section to induce you to infringe any 423 | patents or other property right claims or to contest validity of any 424 | such claims; this section has the sole purpose of protecting the 425 | integrity of the free software distribution system, which is 426 | implemented by public license practices. Many people have made 427 | generous contributions to the wide range of software distributed 428 | through that system in reliance on consistent application of that 429 | system; it is up to the author/donor to decide if he or she is willing 430 | to distribute software through any other system and a licensee cannot 431 | impose that choice. 432 | 433 | This section is intended to make thoroughly clear what is believed to 434 | be a consequence of the rest of this License. 435 | 436 | 8. If the distribution and/or use of the Program is restricted in 437 | certain countries either by patents or by copyrighted interfaces, the 438 | original copyright holder who places the Program under this License 439 | may add an explicit geographical distribution limitation excluding 440 | those countries, so that distribution is permitted only in or among 441 | countries not thus excluded. In such case, this License incorporates 442 | the limitation as if written in the body of this License. 443 | 444 | 9. The Free Software Foundation may publish revised and/or new versions 445 | of the General Public License from time to time. Such new versions will 446 | be similar in spirit to the present version, but may differ in detail to 447 | address new problems or concerns. 448 | 449 | Each version is given a distinguishing version number. If the Program 450 | specifies a version number of this License which applies to it and "any 451 | later version", you have the option of following the terms and conditions 452 | either of that version or of any later version published by the Free 453 | Software Foundation. If the Program does not specify a version number of 454 | this License, you may choose any version ever published by the Free Software 455 | Foundation. 456 | 457 | 10. If you wish to incorporate parts of the Program into other free 458 | programs whose distribution conditions are different, write to the author 459 | to ask for permission. For software which is copyrighted by the Free 460 | Software Foundation, write to the Free Software Foundation; we sometimes 461 | make exceptions for this. Our decision will be guided by the two goals 462 | of preserving the free status of all derivatives of our free software and 463 | of promoting the sharing and reuse of software generally. 464 | 465 | NO WARRANTY 466 | 467 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 468 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 469 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 470 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 471 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 472 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 473 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 474 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 475 | REPAIR OR CORRECTION. 476 | 477 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 478 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 479 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 480 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 481 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 482 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 483 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 484 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 485 | POSSIBILITY OF SUCH DAMAGES. 486 | 487 | */ 488 | (function(e){"undefined"==typeof e.fn.each2&&e.fn.extend({each2:function(h){for(var q=e([0]),m=-1,x=this.length;++ma.length)return[];c=a.split(b);d=0;for(g=c.length;dg?c.push(d(a)):(c.push(d(a.substring(0,g))),c.push(""),c.push(d(a.substring(g,g+b))),c.push(""),c.push(d(a.substring(g+b,a.length))))}function K(a){var b={"\\":"\","&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return String(a).replace(/[&<>"'\/\\]/g,function(a){return b[a]})} 492 | function L(a){var b,c=0,d=null,g=a.quietMillis||100,j=a.url,l=this;return function(k){window.clearTimeout(b);b=window.setTimeout(function(){var b=c+=1,g=a.data,f=j,P=a.transport||e.fn.select2.ajaxDefaults.transport,z=e.extend({},e.fn.select2.ajaxDefaults.params,{type:a.type||"GET",cache:a.cache||!1,jsonpCallback:a.jsonpCallback||h,dataType:a.dataType||"json"}),g=g?g.call(l,k.term,k.page,k.context):null,f="function"===typeof f?f.call(l,k.term,k.page,k.context):f;d&&d.abort();a.params&&(e.isFunction(a.params)? 493 | e.extend(z,a.params.call(l)):e.extend(z,a.params));e.extend(z,{url:f,dataType:a.dataType,data:g,success:function(d){bd.tokenSeparators.length)return h;for(;;){e=-1;k=0;for(n=d.tokenSeparators.length;ke)break;l= 496 | a.substring(0,e);a=a.substring(e+C.length);if(0=a}};v=e(document);var R=1;A=function(){return R++};v.on("mousemove",function(a){y.x=a.pageX;y.y=a.pageY});v=D(Object,{bind:function(a){var b= 498 | this;return function(){a.apply(b,arguments)}},init:function(a){var b,c,d;this.opts=a=this.prepareOpts(a);this.id=a.id;a.element.data("select2")!==h&&null!==a.element.data("select2")&&this.destroy();this.container=this.createContainer();this.containerId="s2id_"+(a.element.attr("id")||"autogen"+A());this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g,"\\$1");this.container.attr("id",this.containerId);var g=!1,j;this.body=function(){!1===g&&(j=a.element.closest("body"), 499 | g=!0);return j};B(this.container,this.opts.element,this.opts.adaptContainerCssClass);this.container.css(r(a.containerCss));this.container.addClass(r(a.containerCssClass));this.elementTabIndex=this.opts.element.attr("tabindex");this.opts.element.data("select2",this).attr("tabindex","-1").before(this.container);this.container.data("select2",this);this.dropdown=this.container.find(".select2-drop");this.dropdown.addClass(r(a.dropdownCssClass));this.dropdown.data("select2",this);this.results=b=this.container.find(".select2-results"); 500 | this.search=c=this.container.find("input.select2-input");this.resultsPage=0;this.context=null;this.initContainer();this.results.on("mousemove",function(a){(y===h||y.x!==a.pageX||y.y!==a.pageY)&&e(a.target).trigger("mousemove-filtered",a)});this.dropdown.on("mousemove-filtered touchstart touchmove touchend",".select2-results",this.bind(this.highlightUnderEvent));var l=this.results,k=I(80,function(a){l.trigger("scroll-debounced",a)});l.on("scroll",function(a){0<=q(a.target,l.get())&&k(a)});this.dropdown.on("scroll-debounced", 501 | ".select2-results",this.bind(this.loadMoreIfNeeded));e(this.container).on("change",".select2-input",function(a){a.stopPropagation()});e(this.dropdown).on("change",".select2-input",function(a){a.stopPropagation()});e.fn.mousewheel&&b.mousewheel(function(a,c,d,e){c=b.scrollTop();0=c-e?(b.scrollTop(0),p(a)):0>e&&b.get(0).scrollHeight-b.scrollTop()+e<=b.height()&&(b.scrollTop(b.get(0).scrollHeight-b.height()),p(a))});H(c);c.on("keyup-change input paste",this.bind(this.updateResults));c.on("focus", 502 | function(){c.addClass("select2-focused")});c.on("blur",function(){c.removeClass("select2-focused")});this.dropdown.on("mouseup",".select2-results",this.bind(function(a){0");d.appendTo("body");var n={width:d.width()-d[0].clientWidth,height:d.height()-d[0].clientHeight};d.remove();d=n}G=d;this.autofocus=a.element.prop("autofocus");a.element.prop("autofocus",!1);this.autofocus&&this.focus()},destroy:function(){var a=this.opts.element,b=a.data("select2");this.propertyObserver&& 504 | (delete this.propertyObserver,this.propertyObserver=null);b!==h&&(b.container.remove(),b.dropdown.remove(),a.removeClass("select2-offscreen").removeData("select2").off(".select2").prop("autofocus",this.autofocus||!1),this.elementTabIndex?a.attr({tabindex:this.elementTabIndex}):a.removeAttr("tabindex"),a.show())},optionToData:function(a){if(a.is("option"))return{id:a.prop("value"),text:a.text(),element:a.get(),css:a.attr("class"),disabled:a.prop("disabled"),locked:m(a.attr("locked"),"locked")};if(a.is("optgroup"))return{text:a.attr("label"), 505 | children:[],element:a.get(),css:a.attr("class")}},prepareOpts:function(a){var b,c,d,g=this;b=a.element;"select"===b.get(0).tagName.toLowerCase()&&(this.select=c=a.element);c&&e.each("id multiple ajax query createSearchChoice initSelection data tags".split(" "),function(){if(this in a)throw Error("Option '"+this+"' is not allowed for Select2 when attached to a element.");});a=e.extend({},{populateResults:function(b,c,d){var n,f=this.opts.id;n=function(b,c,j){var l,p,m,q,r,t,s;b=a.sortResults(b, 506 | c,d);l=0;for(p=b.length;l"),s.addClass("select2-results-dept-"+j),s.addClass("select2-result"),s.addClass(q?"select2-result-selectable":"select2-result-unselectable"),r&&s.addClass("select2-disabled"),t&&s.addClass("select2-result-with-children"),s.addClass(g.opts.formatResultCssClass(m)),q=e(document.createElement("div")),q.addClass("select2-result-label"),r=a.formatResult(m,q,d,g.opts.escapeMarkup),r!== 507 | h&&q.html(r),s.append(q),t&&(t=e(""),t.addClass("select2-result-sub"),n(m.children,t,j+1),s.append(t)),s.data("select2-data",m),c.append(s)};n(c,b,0)}},e.fn.select2.defaults,a);"function"!==typeof a.id&&(d=a.id,a.id=function(a){return a[d]});if(e.isArray(a.element.data("select2Tags"))){if("tags"in a)throw"tags specified as both an attribute 'data-select2-tags' and in options of Select2 "+a.element.attr("id");a.tags=a.element.data("select2Tags")}if(c)a.query=this.bind(function(a){var c={results:[], 508 | more:!1},d=a.term,e,f,m;m=function(b,c){var e;b.is("option")?a.matcher(d,b.text(),b)&&c.push(g.optionToData(b)):b.is("optgroup")&&(e=g.optionToData(b),b.children().each2(function(a,b){m(b,e.children)}),0=this.body().scrollTop(),f=a.outerWidth(!1),m=k+f<=j,p=a.hasClass("select2-drop-above");this.opts.dropdownAutoWidth?(f=e(".select2-results", 515 | a)[0],a.addClass("select2-drop-auto-width"),a.css("width",""),f=a.outerWidth(!1)+(f.scrollHeight===f.clientHeight?0:G.width),f>d?d=f:f=d,m=k+f<=j):this.container.removeClass("select2-drop-auto-width");"static"!==this.body().css("position")&&(j=this.body().offset(),c-=j.top,k-=j.left);p?(j=!0,!h&&l&&(j=!1)):(j=!1,!l&&h&&(j=!0));m||(k=b.left+d-f);j?(c=b.top-g,this.container.addClass("select2-drop-above"),a.addClass("select2-drop-above")):(this.container.removeClass("select2-drop-above"),a.removeClass("select2-drop-above")); 516 | b=e.extend({top:c,left:k,width:d},r(this.opts.dropdownCss));a.css(b)},shouldOpen:function(){var a;if(this.opened()||!1===this._enabled||!0===this._readonly)return!1;a=e.Event("select2-opening");this.opts.element.trigger(a);return!a.isDefaultPrevented()},clearDropdownAlignmentPreference:function(){this.container.removeClass("select2-drop-above");this.dropdown.removeClass("select2-drop-above")},open:function(){if(!this.shouldOpen())return!1;this.opening();return!0},opening:function(){function a(){return{width:Math.max(document.documentElement.scrollWidth, 517 | e(window).width()),height:Math.max(document.documentElement.scrollHeight,e(window).height())}}var b=this.containerId,c="scroll."+b,d="resize."+b,g="orientationchange."+b,j;this.container.addClass("select2-dropdown-open").addClass("select2-container-active");this.clearDropdownAlignmentPreference();this.dropdown[0]!==this.body().children().last()[0]&&this.dropdown.detach().appendTo(this.body());b=e("#select2-drop-mask");0==b.length&&(b=e(document.createElement("div")),b.attr("id","select2-drop-mask").attr("class", 518 | "select2-drop-mask"),b.hide(),b.appendTo(this.body()),b.on("mousedown touchstart click",function(a){var b=e("#select2-drop");0c||(0==c?a.scrollTop(0):(b=this.findHighlightableChoices().find(".select2-result-label"),d=e(b[c]), 521 | g=d.offset().top+d.outerHeight(!0),c===b.length-1&&(b=a.find("li.select2-more-results"),0b&&a.scrollTop(a.scrollTop()+(g-b)),g=d.offset().top-a.offset().top,0>g&&"none"!=d.css("display")&&a.scrollTop(a.scrollTop()+g)))},findHighlightableChoices:function(){return this.results.find(".select2-result-selectable:not(.select2-selected):not(.select2-disabled)")},moveHighlight:function(a){for(var b=this.findHighlightableChoices(), 522 | c=this.highlight();-1=b.length&&(a=b.length-1);0>a&&(a=0);this.results.find(".select2-highlighted").removeClass("select2-highlighted");b=e(b[a]);b.addClass("select2-highlighted");this.ensureHighlightVisible(); 523 | (b=b.data("select2-data"))&&this.opts.element.trigger({type:"select2-highlight",val:this.id(b),choice:b})},countSelectableResults:function(){return this.findHighlightableChoices().length},highlightUnderEvent:function(a){a=e(a.target).closest(".select2-result-selectable");if(0=n&&u(j.formatSelectionTooBig,"formatSelectionTooBig"))){c(""+j.formatSelectionTooBig(n)+"");return}d.val().length"+j.formatInputTooShort(d.val(),j.minimumInputLength)+""):c(""),a&&this.showSearch&&this.showSearch(!0)):j.maximumInputLength&&d.val().length>j.maximumInputLength? 527 | u(j.formatInputTooLong,"formatInputTooLong")?c(""+j.formatInputTooLong(d.val(),j.maximumInputLength)+""):c(""):(j.formatSearching&&0===this.findHighlightableChoices().length&&c(""+j.formatSearching()+""),d.addClass("select2-active"),f=this.tokenize(),f!=h&&null!=f&&d.val(f),this.resultsPage=1,j.query({element:j.element,term:d.val(),page:this.resultsPage,context:null,matcher:j.matcher,callback:this.bind(function(f){var l;this.opened()? 528 | (this.context=f.context===h?null:f.context,this.opts.createSearchChoice&&""!==d.val()&&(l=this.opts.createSearchChoice.call(null,d.val(),f.results),l!==h&&null!==l&&k.id(l)!==h&&null!==k.id(l)&&0===e(f.results).filter(function(){return m(k.id(this),k.id(l))}).length&&f.results.unshift(l)),0===f.results.length&&u(j.formatNoMatches,"formatNoMatches")?c(""+j.formatNoMatches(d.val())+""):(g.empty(),k.opts.populateResults.call(this,g,f.results,{term:d.val(),page:this.resultsPage, 529 | context:null}),!0===f.more&&u(j.formatLoadMore,"formatLoadMore")&&(g.append(""+k.opts.escapeMarkup(j.formatLoadMore(this.resultsPage))+""),window.setTimeout(function(){k.loadMoreIfNeeded()},10)),this.postprocessResults(f,a),b(),this.opts.element.trigger({type:"select2-loaded",data:f}))):this.search.removeClass("select2-active")})}))}},cancel:function(){this.close()},blur:function(){this.opts.selectOnBlur&&this.selectHighlighted({noFocus:!0});this.close();this.container.removeClass("select2-container-active"); 530 | this.search[0]===document.activeElement&&this.search.blur();this.clearSearch();this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus")},focusSearch:function(){var a=this.search;a[0]!==document.activeElement&&window.setTimeout(function(){var b=a[0],c=a.val().length;a.focus();a.is(":visible")&&b===document.activeElement&&(b.setSelectionRange?b.setSelectionRange(c,c):b.createTextRange&&(b=b.createTextRange(),b.collapse(!1),b.select()))},0)},selectHighlighted:function(a){var b= 531 | this.highlight(),c=this.results.find(".select2-highlighted").closest(".select2-result").data("select2-data");c?(this.highlight(b),this.onSelect(c,a)):a.noFocus&&this.close()},getPlaceholder:function(){var a;return this.opts.element.attr("placeholder")||this.opts.element.attr("data-placeholder")||this.opts.element.data("placeholder")||this.opts.placeholder||((a=this.getPlaceholderOption())!==h?a.text():h)},getPlaceholderOption:function(){if(this.select){var a=this.select.children().first();if(this.opts.placeholderOption!== 532 | h)return"first"===this.opts.placeholderOption&&a||"function"===typeof this.opts.placeholderOption&&this.opts.placeholderOption(this.select);if(""===a.text()&&""===a.val())return a}},initContainerWidth:function(){var a=function(){var a,c,d,g;if("off"===this.opts.width)return null;if("element"===this.opts.width)return 0===this.opts.element.outerWidth(!1)?"auto":this.opts.element.outerWidth(!1)+"px";if("copy"===this.opts.width||"resolve"===this.opts.width){a=this.opts.element.attr("style");if(a!==h){a= 533 | a.split(";");d=0;for(g=a.length;d ")}, 534 | enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.focusser.prop("disabled",!this.isInterfaceEnabled())},opening:function(){var a,b;0<=this.opts.minimumResultsForSearch&&this.showSearch(!0);this.parent.opening.apply(this,arguments);!1!==this.showSearchInput&&this.search.val(this.focusser.val());this.search.focus();a=this.search.get(0);a.createTextRange?(a=a.createTextRange(),a.collapse(!1),a.select()):a.setSelectionRange&&(b=this.search.val().length,a.setSelectionRange(b, 535 | b));this.focusser.prop("disabled",!0).val("");this.updateResults(!0);this.opts.element.trigger(e.Event("select2-open"))},close:function(){this.opened()&&(this.parent.close.apply(this,arguments),this.focusser.removeAttr("disabled"),this.focusser.focus())},focus:function(){this.opened()?this.close():(this.focusser.removeAttr("disabled"),this.focusser.focus())},isFocused:function(){return this.container.hasClass("select2-container-active")},cancel:function(){this.parent.cancel.apply(this,arguments); 536 | this.focusser.removeAttr("disabled");this.focusser.focus()},initContainer:function(){var a,b=this.container,c=this.dropdown;0>this.opts.minimumResultsForSearch?this.showSearch(!1):this.showSearch(!0);this.selection=a=b.find(".select2-choice");this.focusser=b.find(".select2-focusser");this.focusser.attr("id","s2id_autogen"+A());e("label[for='"+this.opts.element.attr("id")+"']").attr("for",this.focusser.attr("id"));this.focusser.attr("tabindex",this.elementTabIndex);this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled())if(a.which=== 537 | f.PAGE_UP||a.which===f.PAGE_DOWN)p(a);else switch(a.which){case f.UP:case f.DOWN:this.moveHighlight(a.which===f.UP?-1:1);p(a);break;case f.ENTER:this.selectHighlighted();p(a);break;case f.TAB:this.selectHighlighted({noFocus:!0});break;case f.ESC:this.cancel(a),p(a)}}));this.search.on("blur",this.bind(function(){document.activeElement===this.body().get(0)&&window.setTimeout(this.bind(function(){this.search.focus()}),0)}));this.focusser.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()&& 538 | !(a.which===f.TAB||f.isControl(a)||f.isFunctionKey(a)||a.which===f.ESC))if(!1===this.opts.openOnEnter&&a.which===f.ENTER)p(a);else if(a.which==f.DOWN||a.which==f.UP||a.which==f.ENTER&&this.opts.openOnEnter)!a.altKey&&(!a.ctrlKey&&!a.shiftKey&&!a.metaKey)&&(this.open(),p(a));else if(a.which==f.DELETE||a.which==f.BACKSPACE)this.opts.allowClear&&this.clear(),p(a)}));H(this.focusser);this.focusser.on("keyup-change input",this.bind(function(a){0<=this.opts.minimumResultsForSearch&&(a.stopPropagation(), 539 | this.opened()||this.open())}));a.on("mousedown","abbr",this.bind(function(a){this.isInterfaceEnabled()&&(this.clear(),a.preventDefault(),a.stopImmediatePropagation(),this.close(),this.selection.focus())}));a.on("mousedown",this.bind(function(a){this.container.hasClass("select2-container-active")||this.opts.element.trigger(e.Event("select2-focus"));this.opened()?this.close():this.isInterfaceEnabled()&&this.open();p(a)}));c.on("mousedown",this.bind(function(){this.search.focus()}));a.on("focus",this.bind(function(a){p(a)})); 540 | this.focusser.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(e.Event("select2-focus"));this.container.addClass("select2-container-active")})).on("blur",this.bind(function(){this.opened()||(this.container.removeClass("select2-container-active"),this.opts.element.trigger(e.Event("select2-blur")))}));this.search.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(e.Event("select2-focus")); 541 | this.container.addClass("select2-container-active")}));this.initContainerWidth();this.opts.element.addClass("select2-offscreen");this.setPlaceholder()},clear:function(a){var b=this.selection.data("select2-data");if(b){var c=this.getPlaceholderOption();this.opts.element.val(c?c.val():"");this.selection.find("span").empty();this.selection.removeData("select2-data");this.setPlaceholder();!1!==a&&(this.opts.element.trigger({type:"select2-removed",val:this.id(b),choice:b}),this.triggerChange({removed:b}))}}, 542 | initSelection:function(){if(this.isPlaceholderOptionSelected())this.updateSelection([]),this.close(),this.setPlaceholder();else{var a=this;this.opts.initSelection.call(null,this.opts.element,function(b){b!==h&&null!==b&&(a.updateSelection(b),a.close(),a.setPlaceholder())})}},isPlaceholderOptionSelected:function(){var a;return(a=this.getPlaceholderOption())!==h&&a.is(":selected")||""===this.opts.element.val()||this.opts.element.val()===h||null===this.opts.element.val()},prepareOpts:function(){var a= 543 | this.parent.prepareOpts.apply(this,arguments),b=this;"select"===a.element.get(0).tagName.toLowerCase()?a.initSelection=function(a,d){var e=a.find(":selected");d(b.optionToData(e))}:"data"in a&&(a.initSelection=a.initSelection||function(b,d){var g=b.val(),f=null;a.query({matcher:function(b,c,d){(b=m(g,a.id(d)))&&(f=d);return b},callback:!e.isFunction(d)?e.noop:function(){d(f)}})});return a},getPlaceholder:function(){return this.select&&this.getPlaceholderOption()===h?h:this.parent.getPlaceholder.apply(this, 544 | arguments)},setPlaceholder:function(){var a=this.getPlaceholder();this.isPlaceholderOptionSelected()&&a!==h&&!(this.select&&this.getPlaceholderOption()===h)&&(this.selection.find("span").html(this.opts.escapeMarkup(a)),this.selection.addClass("select2-default"),this.container.removeClass("select2-allowclear"))},postprocessResults:function(a,b,c){var d=0,e=this;this.findHighlightableChoices().each2(function(a,b){if(m(e.id(b.data("select2-data")),e.opts.element.val()))return d=a,!1});!1!==c&&(!0=== 545 | b&&0<=d?this.highlight(d):this.highlight(0));!0===b&&(b=this.opts.minimumResultsForSearch,0<=b&&this.showSearch(O(a.results)>=b))},showSearch:function(a){this.showSearchInput!==a&&(this.showSearchInput=a,this.dropdown.find(".select2-search").toggleClass("select2-search-hidden",!a),this.dropdown.find(".select2-search").toggleClass("select2-offscreen",!a),e(this.dropdown,this.container).toggleClass("select2-with-searchbox",a))},onSelect:function(a,b){if(this.triggerSelect(a)){var c=this.opts.element.val(), 546 | d=this.data();this.opts.element.val(this.id(a));this.updateSelection(a);this.opts.element.trigger({type:"select2-selected",val:this.id(a),choice:a});this.close();(!b||!b.noFocus)&&this.selection.focus();m(c,this.id(a))||this.triggerChange({added:a,removed:d})}},updateSelection:function(a){var b=this.selection.find("span"),c;this.selection.data("select2-data",a);b.empty();c=this.opts.formatSelection(a,b);c!==h&&b.append(this.opts.escapeMarkup(c));a=this.opts.formatSelectionCssClass(a,b);a!==h&&b.addClass(a); 547 | this.selection.removeClass("select2-default");this.opts.allowClear&&this.getPlaceholder()!==h&&this.container.addClass("select2-allowclear")},val:function(){var a,b=!1,c=null,d=this,e=this.data();if(0===arguments.length)return this.opts.element.val();a=arguments[0];1 ")}, 550 | prepareOpts:function(){var a=this.parent.prepareOpts.apply(this,arguments),b=this;"select"===a.element.get(0).tagName.toLowerCase()?a.initSelection=function(a,d){var e=[];a.find(":selected").each2(function(a,c){e.push(b.optionToData(c))});d(e)}:"data"in a&&(a.initSelection=a.initSelection||function(b,d){var g=x(b.val(),a.separator),f=[];a.query({matcher:function(b,c,d){(b=e.grep(g,function(b){return m(b,a.id(d))}).length)&&f.push(d);return b},callback:!e.isFunction(d)?e.noop:function(){for(var b= 551 | [],c=0;cq(d.id(this),b)&&(b.push(d.id(this)),c.push(this))});a=c;this.selection.find(".select2-search-choice").remove();e(a).each(function(){d.addSelectedChoice(this)});d.postprocessResults()},tokenize:function(){var a=this.search.val(),a=this.opts.tokenizer(a,this.data(),this.bind(this.onSelect),this.opts);null!=a&&a!=h&&(this.search.val(a),0=this.getMaximumSelectionSize()&&this.updateResults(!0),this.positionDropdown()):(this.close(), 563 | this.search.width(10)),this.triggerChange({added:a}),(!b||!b.noFocus)&&this.focusSearch())},cancel:function(){this.close();this.focusSearch()},addSelectedChoice:function(a){var b=!a.locked,c=e(" "),d=e(""),c=b?c:d,d=this.id(a),f=this.getVal(),j;j=this.opts.formatSelection(a,c.find("div")); 564 | j!=h&&c.find("div").replaceWith(""+this.opts.escapeMarkup(j)+"");j=this.opts.formatSelectionCssClass(a,c.find("div"));j!=h&&c.addClass(j);if(b)c.find(".select2-search-choice-close").on("mousedown",p).on("click dblclick",this.bind(function(a){this.isInterfaceEnabled()&&(e(a.target).closest(".select2-search-choice").fadeOut("fast",this.bind(function(){this.unselect(e(a.target));this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");this.close();this.focusSearch()})).dequeue(), 565 | p(a))})).on("focus",this.bind(function(){this.isInterfaceEnabled()&&(this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"))}));c.data("select2-data",a);c.insertBefore(this.searchContainer);f.push(d);this.setVal(f)},unselect:function(a){var b=this.getVal(),c,d;a=a.closest(".select2-search-choice");if(0===a.length)throw"Invalid argument: "+a+". Must be .select2-search-choice";if(c=a.data("select2-data"))d=q(this.id(c),b),0<=d&&(b.splice(d,1),this.setVal(b), 566 | this.select&&this.postprocessResults()),a.remove(),this.opts.element.trigger({type:"removed",val:this.id(c),choice:c}),this.triggerChange({removed:c})},postprocessResults:function(a,b,c){var d=this.getVal();b=this.results.find(".select2-result");var e=this.results.find(".select2-result-with-children"),f=this;b.each2(function(a,b){var c=f.id(b.data("select2-data"));0<=q(c,d)&&(b.addClass("select2-selected"),b.find(".select2-result-selectable").addClass("select2-selected"))});e.each2(function(a,b){!b.is(".select2-result-selectable")&& 567 | 0===b.find(".select2-result-selectable:not(.select2-selected)").length&&b.addClass("select2-selected")});-1==this.highlight()&&!1!==c&&f.highlight(0);!this.opts.createSearchChoice&&0"+f.opts.formatNoMatches(f.search.val())+"")},getMaxSearchWidth:function(){return this.selection.width()- 568 | (this.search.outerWidth(!1)-this.search.width())},resizeSearch:function(){var a,b,c,d,f=this.search.outerWidth(!1)-this.search.width();a=this.search;w||(c=a[0].currentStyle||window.getComputedStyle(a[0],null),w=e(document.createElement("div")).css({position:"absolute",left:"-10000px",top:"-10000px",display:"none",fontSize:c.fontSize,fontFamily:c.fontFamily,fontStyle:c.fontStyle,fontWeight:c.fontWeight,letterSpacing:c.letterSpacing,textTransform:c.textTransform,whiteSpace:"nowrap"}),w.attr("class", 569 | "select2-sizer"),e("body").append(w));w.text(a.val());a=w.width()+10;b=this.search.offset().left;c=this.selection.width();d=this.selection.offset().left;b=c-(b-d)-f;bb&&(b=c-f);0>=b&&(b=a);this.search.width(b)},getVal:function(){var a;if(this.select)return a=this.select.val(),null===a?[]:a;a=this.opts.element.val();return x(a,this.opts.separator)},setVal:function(a){var b;this.select?this.select.val(a):(b=[],e(a).each(function(){0>q(this,b)&&b.push(this)}),this.opts.element.val(0=== 570 | b.length?"":b.join(this.opts.separator)))},buildChangeDetails:function(a,b){b=b.slice(0);a=a.slice(0);for(var c=0;c. Attach to instead."); 572 | this.search.width(0);this.searchContainer.hide()},onSortEnd:function(){var a=[],b=this;this.searchContainer.show();this.searchContainer.appendTo(this.searchContainer.parent());this.resizeSearch();this.selection.find(".select2-search-choice").each(function(){a.push(b.opts.id(e(this).data("select2-data")))});this.setVal(a);this.triggerChange()},data:function(a,b){var c=this,d,f;if(0===arguments.length)return this.selection.find(".select2-search-choice").map(function(){return e(this).data("select2-data")}).get(); 573 | f=this.data();a||(a=[]);d=e.map(a,function(a){return c.opts.id(a)});this.setVal(d);this.updateSelection(a);this.clearSearch();b&&this.triggerChange(this.buildChangeDetails(f,this.data()))}});e.fn.select2=function(){var a=Array.prototype.slice.call(arguments,0),b,c,d,f,j,l="val destroy opened open close focus isFocused container dropdown onSortStart onSortEnd enable readonly positionDropdown data search".split(" "),k=["val","opened","isFocused","container","data"],m={search:"externalSearch"};this.each(function(){if(0=== 574 | a.length||"object"===typeof a[0])b=0===a.length?{}:e.extend({},a[0]),b.element=e(this),"select"===b.element.get(0).tagName.toLowerCase()?j=b.element.prop("multiple"):(j=b.multiple||!1,"tags"in b&&(b.multiple=j=!0)),c=j?new F:new E,c.init(b);else if("string"===typeof a[0]){if(0>q(a[0],l))throw"Unknown method: "+a[0];f=h;c=e(this).data("select2");if(c!==h&&(d=a[0],"container"===d?f=c.container:"dropdown"===d?f=c.dropdown:(m[d]&&(d=m[d]),f=c[d].apply(c,a.slice(1))),0<=q(a[0],k)))return!1}else throw"Invalid arguments to select2 plugin: "+ 575 | a;});return f===h?this:f};e.fn.select2.defaults={width:"copy",loadMorePadding:0,closeOnSelect:!0,openOnEnter:!0,containerCss:{},dropdownCss:{},containerCssClass:"",dropdownCssClass:"",formatResult:function(a,b,c,d){b=[];J(a.text,c.term,b,d);return b.join("")},formatSelection:function(a){return a?a.text:h},sortResults:function(a){return a},formatResultCssClass:function(){return h},formatSelectionCssClass:function(){return h},formatNoMatches:function(){return"No matches found"},formatInputTooShort:function(a, 576 | b){var c=b-a.length;return"Please enter "+c+" more character"+(1==c?"":"s")},formatInputTooLong:function(a,b){var c=a.length-b;return"Please delete "+c+" character"+(1==c?"":"s")},formatSelectionTooBig:function(a){return"You can only select "+a+" item"+(1==a?"":"s")},formatLoadMore:function(){return"Loading more results..."},formatSearching:function(){return"Searching..."},minimumResultsForSearch:0,minimumInputLength:0,maximumInputLength:null,maximumSelectionSize:0,id:function(a){return a.id},matcher:function(a, 577 | b){return 0<=(""+b).toUpperCase().indexOf((""+a).toUpperCase())},separator:",",tokenSeparators:[],tokenizer:Q,escapeMarkup:K,blurOnChange:!1,selectOnBlur:!1,adaptContainerCssClass:function(a){return a},adaptDropdownCssClass:function(){return null}};e.fn.select2.ajaxDefaults={transport:e.ajax,params:{type:"GET",cache:!1,dataType:"json"}};window.Select2={query:{ajax:L,local:M,tags:N},util:{debounce:I,markMatch:J,escapeMarkup:K},"class":{"abstract":v,single:E,multi:F}}}})("object"==typeof DjangoSelect2&& 578 | DjangoSelect2.jQuery?DjangoSelect2.jQuery:jQuery); 579 | -------------------------------------------------------------------------------- /select2/static/select2/select2/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Igor Vaynberg 2 | 3 | Version: @@ver@@ Timestamp: @@timestamp@@ 4 | 5 | This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU 6 | General Public License version 2 (the "GPL License"). You may choose either license to govern your 7 | use of this software only upon the condition that you accept all of the terms of either the Apache 8 | License or the GPL License. 9 | 10 | You may obtain a copy of the Apache License and the GPL License at: 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | http://www.gnu.org/licenses/gpl-2.0.html 14 | 15 | Unless required by applicable law or agreed to in writing, software distributed under the Apache License 16 | or the GPL Licesnse is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 17 | either express or implied. See the Apache License and the GPL License for the specific language governing 18 | permissions and limitations under the Apache License and the GPL License. -------------------------------------------------------------------------------- /select2/static/select2/select2/README.md: -------------------------------------------------------------------------------- 1 | Select2 2 | ======= 3 | 4 | Select2 is a jQuery-based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results. 5 | 6 | To get started, checkout examples and documentation at http://ivaynberg.github.com/select2 7 | 8 | Use cases 9 | --------- 10 | 11 | * Enhancing native selects with search. 12 | * Enhancing native selects with a better multi-select interface. 13 | * Loading data from JavaScript: easily load items via ajax and have them searchable. 14 | * Nesting optgroups: native selects only support one level of nested. Select2 does not have this restriction. 15 | * Tagging: ability to add new items on the fly. 16 | * Working with large, remote datasets: ability to partially load a dataset based on the search term. 17 | * Paging of large datasets: easy support for loading more pages when the results are scrolled to the end. 18 | * Templating: support for custom rendering of results and selections. 19 | 20 | Browser compatibility 21 | --------------------- 22 | * IE 8+ 23 | * Chrome 8+ 24 | * Firefox 10+ 25 | * Safari 3+ 26 | * Opera 10.6+ 27 | 28 | Integrations 29 | ------------ 30 | 31 | * [Wicket-Select2](https://github.com/ivaynberg/wicket-select2) (Java / [Apache Wicket](http://wicket.apache.org)) 32 | * [select2-rails](https://github.com/argerim/select2-rails) (Ruby on Rails) 33 | * [AngularUI](http://angular-ui.github.com/#directives-select2) ([AngularJS](angularjs.org)) 34 | * [Django](https://github.com/applegrew/django-select2) 35 | * [Symfony](https://github.com/19Gerhard85/sfSelect2WidgetsPlugin) 36 | * [Bootstrap](https://github.com/t0m/select2-bootstrap-css) (CSS skin) 37 | * [Yii](https://github.com/tonybolzan/yii-select2) 38 | 39 | Internationalization (i18n) 40 | --------------------------- 41 | 42 | Select2 supports multiple languages by simply including the right 43 | language JS file (`select2_locale_it.js`, `select2_locale_nl.js`, etc.). 44 | 45 | Missing a language? Just copy `select2_locale_en.js.template`, translate 46 | it, and make a pull request back to Select2 here on GitHub. 47 | 48 | Bug tracker 49 | ----------- 50 | 51 | Have a bug? Please create an issue here on GitHub! 52 | 53 | https://github.com/ivaynberg/select2/issues 54 | 55 | Mailing list 56 | ------------ 57 | 58 | Have a question? Ask on our mailing list! 59 | 60 | select2@googlegroups.com 61 | 62 | https://groups.google.com/d/forum/select2 63 | 64 | 65 | Copyright and license 66 | --------------------- 67 | 68 | Copyright 2012 Igor Vaynberg 69 | 70 | This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU 71 | General Public License version 2 (the "GPL License"). You may choose either license to govern your 72 | use of this software only upon the condition that you accept all of the terms of either the Apache 73 | License or the GPL License. 74 | 75 | You may obtain a copy of the Apache License and the GPL License in the LICENSE file, or at: 76 | 77 | http://www.apache.org/licenses/LICENSE-2.0 78 | http://www.gnu.org/licenses/gpl-2.0.html 79 | 80 | Unless required by applicable law or agreed to in writing, software distributed under the Apache License 81 | or the GPL License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 82 | either express or implied. See the Apache License and the GPL License for the specific language governing 83 | permissions and limitations under the Apache License and the GPL License. 84 | -------------------------------------------------------------------------------- /select2/static/select2/select2/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "select2", 3 | "version": "3.4.0", 4 | "main": ["select2.js", "select2.css", "select2.png", "select2x2.png", "select2-spinner.gif"], 5 | "dependencies": { 6 | "jquery": ">= 1.7.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /select2/static/select2/select2/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function abspath { 4 | if [[ -d "$1" ]] 5 | then 6 | pushd "$1" >/dev/null 7 | pwd 8 | popd >/dev/null 9 | elif [[ -e $1 ]] 10 | then 11 | pushd $(dirname $1) >/dev/null 12 | echo $(pwd)/$(basename $1) 13 | popd >/dev/null 14 | else 15 | echo $1 does not exist! >&2 16 | return 127 17 | fi 18 | } 19 | 20 | CURR_REL_DIR=$(dirname $0) 21 | CURR_DIR=$(abspath "${CURR_REL_DIR}") 22 | 23 | set -e 24 | 25 | echo -n "Enter the version for this release: " 26 | 27 | read ver 28 | 29 | if [ ! $ver ]; then 30 | echo "Invalid version." 31 | exit 32 | fi 33 | 34 | 35 | name="select2" 36 | js="$CURR_DIR/../js/$name.js" 37 | mini="$CURR_DIR/../js/$name.min.js" 38 | css="$CURR_DIR/../js/$name.css" 39 | timestamp=$(date) 40 | tokens="s/@@ver@@/$ver/g;s/\@@timestamp@@/$timestamp/g" 41 | 42 | cp "$CURR_DIR/$name.js" $js 43 | perl -pi -e 's/(?<=\()jQuery/(typeof DjangoSelect2 == '"'"'object'"'"' && DjangoSelect2.jQuery) ? DjangoSelect2.jQuery : jQuery/g;' $js 44 | 45 | echo "Tokenizing..." 46 | 47 | find . -name "$js" | xargs -I{} sed -e "$tokens" -i "" {} 48 | find . -name "$css" | xargs -I{} sed -e "$tokens" -i "" {} 49 | sed -e "s/latest/$ver/g" -i "" component.json 50 | 51 | echo "Minifying..." 52 | 53 | echo "/*" > "$mini" 54 | cat LICENSE | sed "$tokens" >> "$mini" 55 | echo "*/" >> "$mini" 56 | 57 | curl -s \ 58 | -d compilation_level=SIMPLE_OPTIMIZATIONS \ 59 | -d output_format=text \ 60 | -d output_info=compiled_code \ 61 | --data-urlencode "js_code@$js" \ 62 | http://closure-compiler.appspot.com/compile \ 63 | >> "$mini" 64 | 65 | # perl -pi -e 's/jQuery/(typeof grp == 'object' && DjangoSelect2.jQuery) ? DjangoSelect2.jQuery : jQuery/g;' $mini 66 | 67 | cp $CURR_DIR/*.png $CURR_DIR/../css 68 | cp $CURR_DIR/*.gif $CURR_DIR/../css -------------------------------------------------------------------------------- /select2/static/select2/select2/select2-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theatlantic/django-select2-forms/afa332e6848a3ff2a5df3eb461969815faefb723/select2/static/select2/select2/select2-spinner.gif -------------------------------------------------------------------------------- /select2/static/select2/select2/select2.css: -------------------------------------------------------------------------------- 1 | /* 2 | Version: @@ver@@ Timestamp: @@timestamp@@ 3 | */ 4 | .select2-container { 5 | margin: 0; 6 | position: relative; 7 | display: inline-block; 8 | /* inline-block for ie7 */ 9 | zoom: 1; 10 | *display: inline; 11 | vertical-align: middle; 12 | } 13 | 14 | .select2-container, 15 | .select2-drop, 16 | .select2-search, 17 | .select2-search input{ 18 | /* 19 | Force border-box so that % widths fit the parent 20 | container without overlap because of margin/padding. 21 | 22 | More Info : http://www.quirksmode.org/css/box.html 23 | */ 24 | -webkit-box-sizing: border-box; /* webkit */ 25 | -khtml-box-sizing: border-box; /* konqueror */ 26 | -moz-box-sizing: border-box; /* firefox */ 27 | -ms-box-sizing: border-box; /* ie */ 28 | box-sizing: border-box; /* css3 */ 29 | } 30 | 31 | .select2-container .select2-choice { 32 | display: block; 33 | height: 26px; 34 | padding: 0 0 0 8px; 35 | overflow: hidden; 36 | position: relative; 37 | 38 | border: 1px solid #aaa; 39 | white-space: nowrap; 40 | line-height: 26px; 41 | color: #444; 42 | text-decoration: none; 43 | 44 | -webkit-border-radius: 4px; 45 | -moz-border-radius: 4px; 46 | border-radius: 4px; 47 | 48 | -webkit-background-clip: padding-box; 49 | -moz-background-clip: padding; 50 | background-clip: padding-box; 51 | 52 | -webkit-touch-callout: none; 53 | -webkit-user-select: none; 54 | -khtml-user-select: none; 55 | -moz-user-select: none; 56 | -ms-user-select: none; 57 | user-select: none; 58 | 59 | background-color: #fff; 60 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.5, white)); 61 | background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 50%); 62 | background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 50%); 63 | background-image: -o-linear-gradient(bottom, #eeeeee 0%, #ffffff 50%); 64 | background-image: -ms-linear-gradient(top, #ffffff 0%, #eeeeee 50%); 65 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0); 66 | background-image: linear-gradient(top, #ffffff 0%, #eeeeee 50%); 67 | } 68 | 69 | .select2-container.select2-drop-above .select2-choice { 70 | border-bottom-color: #aaa; 71 | 72 | -webkit-border-radius:0 0 4px 4px; 73 | -moz-border-radius:0 0 4px 4px; 74 | border-radius:0 0 4px 4px; 75 | 76 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.9, white)); 77 | background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 90%); 78 | background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 90%); 79 | background-image: -o-linear-gradient(bottom, #eeeeee 0%, white 90%); 80 | background-image: -ms-linear-gradient(top, #eeeeee 0%,#ffffff 90%); 81 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 ); 82 | background-image: linear-gradient(top, #eeeeee 0%,#ffffff 90%); 83 | } 84 | 85 | .select2-container.select2-allowclear .select2-choice span { 86 | margin-right: 42px; 87 | } 88 | 89 | .select2-container .select2-choice span { 90 | margin-right: 26px; 91 | display: block; 92 | overflow: hidden; 93 | 94 | white-space: nowrap; 95 | 96 | -ms-text-overflow: ellipsis; 97 | -o-text-overflow: ellipsis; 98 | text-overflow: ellipsis; 99 | } 100 | 101 | .select2-container .select2-choice abbr { 102 | display: none; 103 | width: 12px; 104 | height: 12px; 105 | position: absolute; 106 | right: 24px; 107 | top: 8px; 108 | 109 | font-size: 1px; 110 | text-decoration: none; 111 | 112 | border: 0; 113 | background: url('select2.png') right top no-repeat; 114 | cursor: pointer; 115 | outline: 0; 116 | } 117 | 118 | .select2-container.select2-allowclear .select2-choice abbr { 119 | display: inline-block; 120 | } 121 | 122 | .select2-container .select2-choice abbr:hover { 123 | background-position: right -11px; 124 | cursor: pointer; 125 | } 126 | 127 | .select2-drop-undermask { 128 | border: 0; 129 | margin: 0; 130 | padding: 0; 131 | position: absolute; 132 | left: 0; 133 | top: 0; 134 | z-index: 9998; 135 | background-color: transparent; 136 | filter: alpha(opacity=0); 137 | } 138 | 139 | .select2-drop-mask { 140 | border: 0; 141 | margin: 0; 142 | padding: 0; 143 | position: absolute; 144 | left: 0; 145 | top: 0; 146 | z-index: 9998; 147 | /* styles required for IE to work */ 148 | background-color: #fff; 149 | opacity: 0; 150 | filter: alpha(opacity=0); 151 | } 152 | 153 | .select2-drop { 154 | width: 100%; 155 | margin-top:-1px; 156 | position: absolute; 157 | z-index: 9999; 158 | top: 100%; 159 | 160 | background: #fff; 161 | color: #000; 162 | border: 1px solid #aaa; 163 | border-top: 0; 164 | 165 | -webkit-border-radius: 0 0 4px 4px; 166 | -moz-border-radius: 0 0 4px 4px; 167 | border-radius: 0 0 4px 4px; 168 | 169 | -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); 170 | -moz-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); 171 | box-shadow: 0 4px 5px rgba(0, 0, 0, .15); 172 | } 173 | 174 | .select2-drop-auto-width { 175 | border-top: 1px solid #aaa; 176 | width: auto; 177 | } 178 | 179 | .select2-drop-auto-width .select2-search { 180 | padding-top: 4px; 181 | } 182 | 183 | .select2-drop.select2-drop-above { 184 | margin-top: 1px; 185 | border-top: 1px solid #aaa; 186 | border-bottom: 0; 187 | 188 | -webkit-border-radius: 4px 4px 0 0; 189 | -moz-border-radius: 4px 4px 0 0; 190 | border-radius: 4px 4px 0 0; 191 | 192 | -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); 193 | -moz-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); 194 | box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); 195 | } 196 | 197 | .select2-drop-active { 198 | border: 1px solid #5897fb; 199 | border-top: none; 200 | } 201 | 202 | .select2-drop.select2-drop-above.select2-drop-active { 203 | border-top: 1px solid #5897fb; 204 | } 205 | 206 | .select2-container .select2-choice div { 207 | display: inline-block; 208 | width: 18px; 209 | height: 100%; 210 | position: absolute; 211 | right: 0; 212 | top: 0; 213 | 214 | border-left: 1px solid #aaa; 215 | -webkit-border-radius: 0 4px 4px 0; 216 | -moz-border-radius: 0 4px 4px 0; 217 | border-radius: 0 4px 4px 0; 218 | 219 | -webkit-background-clip: padding-box; 220 | -moz-background-clip: padding; 221 | background-clip: padding-box; 222 | 223 | background: #ccc; 224 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee)); 225 | background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%); 226 | background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%); 227 | background-image: -o-linear-gradient(bottom, #ccc 0%, #eee 60%); 228 | background-image: -ms-linear-gradient(top, #cccccc 0%, #eeeeee 60%); 229 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#cccccc', GradientType = 0); 230 | background-image: linear-gradient(top, #cccccc 0%, #eeeeee 60%); 231 | } 232 | 233 | .select2-container .select2-choice div b { 234 | display: block; 235 | width: 100%; 236 | height: 100%; 237 | background: url('select2.png') no-repeat 0 1px; 238 | } 239 | 240 | .select2-search { 241 | display: inline-block; 242 | width: 100%; 243 | min-height: 26px; 244 | margin: 0; 245 | padding-left: 4px; 246 | padding-right: 4px; 247 | 248 | position: relative; 249 | z-index: 10000; 250 | 251 | white-space: nowrap; 252 | } 253 | 254 | .select2-search input { 255 | width: 100%; 256 | height: auto !important; 257 | min-height: 26px; 258 | padding: 4px 20px 4px 5px; 259 | margin: 0; 260 | 261 | outline: 0; 262 | font-family: sans-serif; 263 | font-size: 1em; 264 | 265 | border: 1px solid #aaa; 266 | -webkit-border-radius: 0; 267 | -moz-border-radius: 0; 268 | border-radius: 0; 269 | 270 | -webkit-box-shadow: none; 271 | -moz-box-shadow: none; 272 | box-shadow: none; 273 | 274 | background: #fff url('select2.png') no-repeat 100% -22px; 275 | background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); 276 | background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); 277 | background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); 278 | background: url('select2.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); 279 | background: url('select2.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%); 280 | background: url('select2.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%, #eeeeee 99%); 281 | } 282 | 283 | .select2-drop.select2-drop-above .select2-search input { 284 | margin-top: 4px; 285 | } 286 | 287 | .select2-search input.select2-active { 288 | background: #fff url('select2-spinner.gif') no-repeat 100%; 289 | background: url('select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); 290 | background: url('select2-spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); 291 | background: url('select2-spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); 292 | background: url('select2-spinner.gif') no-repeat 100%, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); 293 | background: url('select2-spinner.gif') no-repeat 100%, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%); 294 | background: url('select2-spinner.gif') no-repeat 100%, linear-gradient(top, #ffffff 85%, #eeeeee 99%); 295 | } 296 | 297 | .select2-container-active .select2-choice, 298 | .select2-container-active .select2-choices { 299 | border: 1px solid #5897fb; 300 | outline: none; 301 | 302 | -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); 303 | -moz-box-shadow: 0 0 5px rgba(0,0,0,.3); 304 | box-shadow: 0 0 5px rgba(0,0,0,.3); 305 | } 306 | 307 | .select2-dropdown-open .select2-choice { 308 | border-bottom-color: transparent; 309 | -webkit-box-shadow: 0 1px 0 #fff inset; 310 | -moz-box-shadow: 0 1px 0 #fff inset; 311 | box-shadow: 0 1px 0 #fff inset; 312 | 313 | -webkit-border-bottom-left-radius: 0; 314 | -moz-border-radius-bottomleft: 0; 315 | border-bottom-left-radius: 0; 316 | 317 | -webkit-border-bottom-right-radius: 0; 318 | -moz-border-radius-bottomright: 0; 319 | border-bottom-right-radius: 0; 320 | 321 | background-color: #eee; 322 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, white), color-stop(0.5, #eeeeee)); 323 | background-image: -webkit-linear-gradient(center bottom, white 0%, #eeeeee 50%); 324 | background-image: -moz-linear-gradient(center bottom, white 0%, #eeeeee 50%); 325 | background-image: -o-linear-gradient(bottom, white 0%, #eeeeee 50%); 326 | background-image: -ms-linear-gradient(top, #ffffff 0%,#eeeeee 50%); 327 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 ); 328 | background-image: linear-gradient(top, #ffffff 0%,#eeeeee 50%); 329 | } 330 | 331 | .select2-dropdown-open.select2-drop-above .select2-choice, 332 | .select2-dropdown-open.select2-drop-above .select2-choices { 333 | border: 1px solid #5897fb; 334 | border-top-color: transparent; 335 | 336 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, white), color-stop(0.5, #eeeeee)); 337 | background-image: -webkit-linear-gradient(center top, white 0%, #eeeeee 50%); 338 | background-image: -moz-linear-gradient(center top, white 0%, #eeeeee 50%); 339 | background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); 340 | background-image: -ms-linear-gradient(bottom, #ffffff 0%,#eeeeee 50%); 341 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 ); 342 | background-image: linear-gradient(bottom, #ffffff 0%,#eeeeee 50%); 343 | } 344 | 345 | .select2-dropdown-open .select2-choice div { 346 | background: transparent; 347 | border-left: none; 348 | filter: none; 349 | } 350 | .select2-dropdown-open .select2-choice div b { 351 | background-position: -18px 1px; 352 | } 353 | 354 | /* results */ 355 | .select2-results { 356 | max-height: 200px; 357 | padding: 0 0 0 4px; 358 | margin: 4px 4px 4px 0; 359 | position: relative; 360 | overflow-x: hidden; 361 | overflow-y: auto; 362 | -webkit-tap-highlight-color: rgba(0,0,0,0); 363 | } 364 | 365 | .select2-results ul.select2-result-sub { 366 | margin: 0; 367 | padding-left: 0; 368 | } 369 | 370 | .select2-results ul.select2-result-sub > li .select2-result-label { padding-left: 20px } 371 | .select2-results ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 40px } 372 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 60px } 373 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 80px } 374 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 100px } 375 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 110px } 376 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 120px } 377 | 378 | .select2-results li { 379 | list-style: none; 380 | display: list-item; 381 | background-image: none; 382 | } 383 | 384 | .select2-results li.select2-result-with-children > .select2-result-label { 385 | font-weight: bold; 386 | } 387 | 388 | .select2-results .select2-result-label { 389 | padding: 3px 7px 4px; 390 | margin: 0; 391 | cursor: pointer; 392 | 393 | min-height: 1em; 394 | 395 | -webkit-touch-callout: none; 396 | -webkit-user-select: none; 397 | -khtml-user-select: none; 398 | -moz-user-select: none; 399 | -ms-user-select: none; 400 | user-select: none; 401 | } 402 | 403 | .select2-results .select2-highlighted { 404 | background: #3875d7; 405 | color: #fff; 406 | } 407 | 408 | .select2-results li em { 409 | background: #feffde; 410 | font-style: normal; 411 | } 412 | 413 | .select2-results .select2-highlighted em { 414 | background: transparent; 415 | } 416 | 417 | .select2-results .select2-highlighted ul { 418 | background: white; 419 | color: #000; 420 | } 421 | 422 | 423 | .select2-results .select2-no-results, 424 | .select2-results .select2-searching, 425 | .select2-results .select2-selection-limit { 426 | background: #f4f4f4; 427 | display: list-item; 428 | } 429 | 430 | /* 431 | disabled look for disabled choices in the results dropdown 432 | */ 433 | .select2-results .select2-disabled.select2-highlighted { 434 | color: #666; 435 | background: #f4f4f4; 436 | display: list-item; 437 | cursor: default; 438 | } 439 | .select2-results .select2-disabled { 440 | background: #f4f4f4; 441 | display: list-item; 442 | cursor: default; 443 | } 444 | 445 | .select2-results .select2-selected { 446 | display: none; 447 | } 448 | 449 | .select2-more-results.select2-active { 450 | background: #f4f4f4 url('select2-spinner.gif') no-repeat 100%; 451 | } 452 | 453 | .select2-more-results { 454 | background: #f4f4f4; 455 | display: list-item; 456 | } 457 | 458 | /* disabled styles */ 459 | 460 | .select2-container.select2-container-disabled .select2-choice { 461 | background-color: #f4f4f4; 462 | background-image: none; 463 | border: 1px solid #ddd; 464 | cursor: default; 465 | } 466 | 467 | .select2-container.select2-container-disabled .select2-choice div { 468 | background-color: #f4f4f4; 469 | background-image: none; 470 | border-left: 0; 471 | } 472 | 473 | .select2-container.select2-container-disabled .select2-choice abbr { 474 | display: none; 475 | } 476 | 477 | 478 | /* multiselect */ 479 | 480 | .select2-container-multi .select2-choices { 481 | height: auto !important; 482 | height: 1%; 483 | margin: 0; 484 | padding: 0; 485 | position: relative; 486 | 487 | border: 1px solid #aaa; 488 | cursor: text; 489 | overflow: hidden; 490 | 491 | background-color: #fff; 492 | background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff)); 493 | background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 494 | background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 495 | background-image: -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 496 | background-image: -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 497 | background-image: linear-gradient(top, #eeeeee 1%, #ffffff 15%); 498 | } 499 | 500 | .select2-locked { 501 | padding: 3px 5px 3px 5px !important; 502 | } 503 | 504 | .select2-container-multi .select2-choices { 505 | min-height: 26px; 506 | } 507 | 508 | .select2-container-multi.select2-container-active .select2-choices { 509 | border: 1px solid #5897fb; 510 | outline: none; 511 | 512 | -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); 513 | -moz-box-shadow: 0 0 5px rgba(0,0,0,.3); 514 | box-shadow: 0 0 5px rgba(0,0,0,.3); 515 | } 516 | .select2-container-multi .select2-choices li { 517 | float: left; 518 | list-style: none; 519 | } 520 | .select2-container-multi .select2-choices .select2-search-field { 521 | margin: 0; 522 | padding: 0; 523 | white-space: nowrap; 524 | } 525 | 526 | .select2-container-multi .select2-choices .select2-search-field input { 527 | padding: 5px; 528 | margin: 1px 0; 529 | 530 | font-family: sans-serif; 531 | font-size: 100%; 532 | color: #666; 533 | outline: 0; 534 | border: 0; 535 | -webkit-box-shadow: none; 536 | -moz-box-shadow: none; 537 | box-shadow: none; 538 | background: transparent !important; 539 | } 540 | 541 | .select2-container-multi .select2-choices .select2-search-field input.select2-active { 542 | background: #fff url('select2-spinner.gif') no-repeat 100% !important; 543 | } 544 | 545 | .select2-default { 546 | color: #999 !important; 547 | } 548 | 549 | .select2-container-multi .select2-choices .select2-search-choice { 550 | padding: 3px 5px 3px 18px; 551 | margin: 3px 0 3px 5px; 552 | position: relative; 553 | 554 | line-height: 13px; 555 | color: #333; 556 | cursor: default; 557 | border: 1px solid #aaaaaa; 558 | 559 | -webkit-border-radius: 3px; 560 | -moz-border-radius: 3px; 561 | border-radius: 3px; 562 | 563 | -webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); 564 | -moz-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); 565 | box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); 566 | 567 | -webkit-background-clip: padding-box; 568 | -moz-background-clip: padding; 569 | background-clip: padding-box; 570 | 571 | -webkit-touch-callout: none; 572 | -webkit-user-select: none; 573 | -khtml-user-select: none; 574 | -moz-user-select: none; 575 | -ms-user-select: none; 576 | user-select: none; 577 | 578 | background-color: #e4e4e4; 579 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0 ); 580 | background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee)); 581 | background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 582 | background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 583 | background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 584 | background-image: -ms-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 585 | background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 586 | } 587 | .select2-container-multi .select2-choices .select2-search-choice span { 588 | cursor: default; 589 | } 590 | .select2-container-multi .select2-choices .select2-search-choice-focus { 591 | background: #d4d4d4; 592 | } 593 | 594 | .select2-search-choice-close { 595 | display: block; 596 | width: 12px; 597 | height: 13px; 598 | position: absolute; 599 | right: 3px; 600 | top: 4px; 601 | 602 | font-size: 1px; 603 | outline: none; 604 | background: url('select2.png') right top no-repeat; 605 | } 606 | 607 | .select2-container-multi .select2-search-choice-close { 608 | left: 3px; 609 | } 610 | 611 | .select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover { 612 | background-position: right -11px; 613 | } 614 | .select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close { 615 | background-position: right -11px; 616 | } 617 | 618 | /* disabled styles */ 619 | .select2-container-multi.select2-container-disabled .select2-choices{ 620 | background-color: #f4f4f4; 621 | background-image: none; 622 | border: 1px solid #ddd; 623 | cursor: default; 624 | } 625 | 626 | .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice { 627 | padding: 3px 5px 3px 5px; 628 | border: 1px solid #ddd; 629 | background-image: none; 630 | background-color: #f4f4f4; 631 | } 632 | 633 | .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { display: none; 634 | background:none; 635 | } 636 | /* end multiselect */ 637 | 638 | 639 | .select2-result-selectable .select2-match, 640 | .select2-result-unselectable .select2-match { 641 | text-decoration: underline; 642 | } 643 | 644 | .select2-offscreen, .select2-offscreen:focus { 645 | clip: rect(0 0 0 0); 646 | width: 1px; 647 | height: 1px; 648 | border: 0; 649 | margin: 0; 650 | padding: 0; 651 | overflow: hidden; 652 | position: absolute; 653 | outline: 0; 654 | left: 0px; 655 | } 656 | 657 | .select2-display-none { 658 | display: none; 659 | } 660 | 661 | .select2-measure-scrollbar { 662 | position: absolute; 663 | top: -10000px; 664 | left: -10000px; 665 | width: 100px; 666 | height: 100px; 667 | overflow: scroll; 668 | } 669 | /* Retina-ize icons */ 670 | 671 | @media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi) { 672 | .select2-search input, .select2-search-choice-close, .select2-container .select2-choice abbr, .select2-container .select2-choice div b { 673 | background-image: url('select2x2.png') !important; 674 | background-repeat: no-repeat !important; 675 | background-size: 60px 40px !important; 676 | } 677 | .select2-search input { 678 | background-position: 100% -21px !important; 679 | } 680 | } 681 | -------------------------------------------------------------------------------- /select2/static/select2/select2/select2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theatlantic/django-select2-forms/afa332e6848a3ff2a5df3eb461969815faefb723/select2/static/select2/select2/select2.png -------------------------------------------------------------------------------- /select2/static/select2/select2/select2x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theatlantic/django-select2-forms/afa332e6848a3ff2a5df3eb461969815faefb723/select2/static/select2/select2/select2x2.png -------------------------------------------------------------------------------- /select2/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | import select2.views 4 | 5 | 6 | urlpatterns = [ 7 | re_path(r'^fetch_items/(?P[^\/]+)/(?P[^\/]+)/(?P[^\/]+)/$', 8 | select2.views.fetch_items, name='select2_fetch_items'), 9 | re_path(r'^init_selection/(?P[^\/]+)/(?P[^\/]+)/(?P[^\/]+)/$', 10 | select2.views.init_selection, name='select2_init_selection'), 11 | ] 12 | -------------------------------------------------------------------------------- /select2/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | re_spaces = re.compile(r"\s+") 4 | 5 | 6 | def combine_css_classes(classes, new_classes): 7 | if not classes: 8 | if isinstance(new_classes, str): 9 | return new_classes 10 | else: 11 | try: 12 | return u" ".join(new_classes) 13 | except TypeError: 14 | return new_classes 15 | 16 | if isinstance(classes, str): 17 | classes = set(re_spaces.split(classes)) 18 | else: 19 | try: 20 | classes = set(classes) 21 | except TypeError: 22 | return classes 23 | 24 | if isinstance(new_classes, str): 25 | new_classes = set(re_spaces.split(new_classes)) 26 | else: 27 | try: 28 | new_classes = set(new_classes) 29 | except TypeError: 30 | return u" ".join(classes) 31 | 32 | return u" ".join(classes.union(new_classes)) 33 | -------------------------------------------------------------------------------- /select2/views.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | 4 | from django.db import models 5 | from django.apps import apps 6 | from django.forms.models import ModelChoiceIterator 7 | from django.http import HttpResponse 8 | from django.utils.encoding import force_str 9 | try: 10 | from django.forms.models import ModelChoiceIteratorValue 11 | except ImportError: 12 | ModelChoiceIteratorValue = None 13 | 14 | from .fields import ManyToManyField, compat_rel 15 | 16 | 17 | class ViewException(Exception): 18 | pass 19 | 20 | 21 | class InvalidParameter(ViewException): 22 | pass 23 | 24 | 25 | class JsonResponse(HttpResponse): 26 | 27 | callback = None 28 | 29 | def __init__(self, content='', callback=None, content_type="application/json", *args, **kwargs): 30 | if not isinstance(content, str): 31 | content = json.dumps(content) 32 | if callback is not None: 33 | self.callback = callback 34 | if self.callback is not None: 35 | content = u"%s(\n%s\n)" % (self.callback, content) 36 | content_type = "text/javascript" 37 | return super(JsonResponse, self).__init__(content=content, 38 | content_type=content_type, *args, **kwargs) 39 | 40 | 41 | class Select2View(object): 42 | 43 | def __init__(self, request, app_label, model_name, field_name): 44 | self.request = request 45 | self.app_label = app_label 46 | self.model_name = model_name 47 | self.field_name = field_name 48 | 49 | _field = None 50 | 51 | def get_field_and_model(self): 52 | model_cls = apps.get_model(self.app_label, self.model_name) 53 | if model_cls is None: 54 | raise ViewException('Model %s.%s does not exist' % (self.app_label, self.model_name)) 55 | if self._field is None: 56 | self._field = model_cls._meta.get_field(self.field_name) 57 | return self._field, model_cls 58 | 59 | def get_response(self, data, **kwargs): 60 | callback = self.request.GET.get('callback', None) 61 | if callback is None: 62 | response_cls = JsonResponse 63 | else: 64 | response_cls = type('JsonpResponse', (JsonResponse,), { 65 | 'callback': callback, 66 | }) 67 | return response_cls(data, **kwargs) 68 | 69 | def get_data(self, queryset, page=None, page_limit=None): 70 | field, model_cls = self.get_field_and_model() 71 | 72 | # Check for the existences of a callable %s_queryset method on the 73 | # model class and use it to filter the Select2 queryset. 74 | # 75 | # This is useful for model inheritance where the limit_choices_to can 76 | # not easily be overriden in child classes. 77 | model_queryset_method = '%s_queryset' % field.name 78 | if callable(getattr(model_cls, model_queryset_method, None)): 79 | queryset = getattr(model_cls, model_queryset_method)(queryset) 80 | 81 | formfield = field.formfield() 82 | total_count = None 83 | if page is not None and page_limit is not None: 84 | total_count = queryset.count() 85 | offset = (page - 1) * page_limit 86 | end = offset + page_limit 87 | queryset = queryset[offset:end] 88 | else: 89 | offset = None 90 | 91 | formfield.queryset = queryset 92 | iterator = ModelChoiceIterator(formfield) 93 | 94 | if offset is None: 95 | total_count = len(iterator) 96 | more = False 97 | else: 98 | paged_count = offset + len(iterator) 99 | more = bool(paged_count < total_count) 100 | 101 | data = { 102 | 'total': total_count, 103 | 'more': more, 104 | 'results': [], 105 | } 106 | for value, label in iterator: 107 | if value is u'': 108 | continue 109 | 110 | if ModelChoiceIteratorValue and isinstance(value, ModelChoiceIteratorValue): 111 | # ModelChoiceIteratorValue was added in Django 3.1 112 | value = value.value 113 | 114 | data['results'].append({ 115 | 'id': value, 116 | 'text': label, 117 | }) 118 | return data 119 | 120 | def init_selection(self): 121 | try: 122 | field, model_cls = self.get_field_and_model() 123 | except ViewException as e: 124 | return self.get_response({'error': str(e)}, status=500) 125 | 126 | q = self.request.GET.get('q', None) 127 | try: 128 | if q is None: 129 | raise InvalidParameter("q parameter required") 130 | pks = q.split(u',') 131 | try: 132 | pks = [int(pk) for pk in pks] 133 | except TypeError: 134 | raise InvalidParameter("q parameter must be comma separated " 135 | "list of integers") 136 | except InvalidParameter as e: 137 | return self.get_response({'error': str(e)}, status=500) 138 | 139 | queryset = field.queryset.filter(**{ 140 | (u'%s__in' % compat_rel(field).get_related_field().name): pks, 141 | }).distinct() 142 | pk_ordering = dict([(force_str(pk), i) for i, pk in enumerate(pks)]) 143 | 144 | data = self.get_data(queryset) 145 | 146 | # Make sure we return in the same order we were passed 147 | def results_sort_callback(item): 148 | pk = force_str(item['id']) 149 | return pk_ordering[pk] 150 | data['results'] = sorted(data['results'], key=results_sort_callback) 151 | 152 | if len(data['results']) == 1: 153 | is_multiple = isinstance(field, ManyToManyField) 154 | try: 155 | multiple_param = int(self.request.GET.get('multiple')) 156 | except (TypeError, ValueError): 157 | pass 158 | else: 159 | is_multiple = (multiple_param == 1) 160 | if not is_multiple: 161 | data['results'] = data['results'][0] 162 | 163 | return self.get_response(data) 164 | 165 | def fetch_items(self): 166 | try: 167 | field, model_cls = self.get_field_and_model() 168 | except ViewException as e: 169 | return self.get_response({'error': str(e)}, status=500) 170 | 171 | queryset = copy.deepcopy(field.queryset) 172 | 173 | q = self.request.GET.get('q', None) 174 | page_limit = self.request.GET.get('page_limit', 10) 175 | page = self.request.GET.get('page', 1) 176 | 177 | try: 178 | if q is None: 179 | raise InvalidParameter("q parameter required") 180 | try: 181 | page_limit = int(page_limit) 182 | except TypeError: 183 | raise InvalidParameter("Invalid page_limit '%s' passed" % page_limit) 184 | else: 185 | if page_limit < 1: 186 | raise InvalidParameter("Invalid page_limit '%s' passed" % page_limit) 187 | 188 | try: 189 | page = int(page) 190 | except TypeError: 191 | raise InvalidParameter("Invalid page '%s' passed") 192 | else: 193 | if page < 1: 194 | raise InvalidParameter("Invalid page '%s' passed") 195 | except InvalidParameter as e: 196 | return self.get_response({'error': str(e)}, status=500) 197 | 198 | search_field = field.search_field 199 | if callable(search_field): 200 | search_field = search_field(q) 201 | if isinstance(search_field, models.Q): 202 | q_obj = search_field 203 | else: 204 | qset_contains_filter_key = '%(search_field)s__%(insensitive)scontains' % { 205 | 'search_field': search_field, 206 | 'insensitive': 'i' if not field.case_sensitive else '', 207 | } 208 | q_obj = models.Q(**{qset_contains_filter_key: q}) 209 | 210 | queryset = queryset.filter(q_obj) 211 | 212 | data = self.get_data(queryset, page, page_limit) 213 | return self.get_response(data) 214 | 215 | 216 | def init_selection(request, app_label, model_name, field_name): 217 | view_cls = Select2View(request, app_label, model_name, field_name) 218 | return view_cls.init_selection() 219 | 220 | 221 | def fetch_items(request, app_label, model_name, field_name): 222 | view_cls = Select2View(request, app_label, model_name, field_name) 223 | return view_cls.fetch_items() 224 | -------------------------------------------------------------------------------- /select2/widgets.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | import json 3 | 4 | import django 5 | from django.urls import reverse 6 | from django.forms import widgets 7 | from django.forms.utils import flatatt 8 | from django.utils.datastructures import MultiValueDict 9 | from django.utils.html import escape, conditional_escape 10 | from django.utils.encoding import force_str 11 | from django.utils.safestring import mark_safe 12 | 13 | from .utils import combine_css_classes 14 | 15 | 16 | __all__ = ('Select', 'SelectMultiple',) 17 | 18 | 19 | class Select(widgets.Input): 20 | 21 | allow_multiple_selected = False 22 | 23 | class Media: 24 | js = ( 25 | "admin/js/jquery.init.js", 26 | "select2/js/select2.jquery_ready.js", 27 | "select2/js/select2.jquery_ui_sortable.js", 28 | "select2/js/select2.js", 29 | ) 30 | css = { 31 | "all": ( 32 | "select2/css/select2.css", 33 | )} 34 | 35 | js_options_map = { 36 | 'maximum_selection_size': 'maximumSelectionSize', 37 | 'allow_clear': 'allowClear', 38 | 'minimum_input_length': 'minimumInputLength', 39 | 'minimum_results_for_search': 'minimumResultsForSearch', 40 | 'close_on_select': 'closeOnSelect', 41 | 'open_on_enter': 'openOnEnter', 42 | 'token_separators': 'tokenSeparators', 43 | 'ajax_quiet_millis': 'quietMillis', 44 | 'quiet_millis': 'quietMillis', 45 | 'data_type': 'dataType', 46 | } 47 | 48 | js_options = None 49 | sortable = False 50 | default_class = ('django-select2',) 51 | ajax = False 52 | 53 | def __init__(self, attrs=None, choices=(), js_options=None, *args, **kwargs): 54 | self.ajax = kwargs.pop('ajax', self.ajax) 55 | self.js_options = {} 56 | if js_options is not None: 57 | for k, v in js_options.items(): 58 | if k in self.js_options_map: 59 | k = self.js_options_map[k] 60 | self.js_options[k] = v 61 | 62 | attrs = attrs.copy() if attrs is not None else {} 63 | 64 | if 'sortable' in kwargs: 65 | self.sortable = kwargs.pop('sortable') 66 | 67 | self.attrs = getattr(self, 'attrs', {}) or {} 68 | 69 | self.attrs.update({ 70 | 'data-placeholder': kwargs.pop('overlay', None), 71 | 'class': combine_css_classes(attrs.get('class', ''), self.default_class), 72 | 'data-sortable': json.dumps(self.sortable), 73 | }) 74 | 75 | self.attrs.update(attrs) 76 | self.choices = iter(choices) 77 | 78 | def reverse(self, lookup_view): 79 | opts = getattr(self, 'model', self.field.model)._meta 80 | return reverse(lookup_view, kwargs={ 81 | 'app_label': opts.app_label, 82 | 'model_name': opts.object_name.lower(), 83 | 'field_name': self.field.name, 84 | }) 85 | 86 | def option_to_data(self, option_value, option_label): 87 | if not option_value: 88 | return 89 | if isinstance(option_label, (list, tuple)): 90 | return { 91 | "text": force_str(option_value), 92 | "children": [_f for _f in [self.option_to_data(v, l) for v, l in option_label] if _f], 93 | } 94 | return { 95 | "id": force_str(option_value), 96 | "text": force_str(option_label), 97 | } 98 | 99 | def render(self, name, value, attrs=None, choices=(), js_options=None, **kwargs): 100 | options = {} 101 | attrs = dict(self.attrs, **(attrs or {})) 102 | js_options = js_options or {} 103 | 104 | for k, v in dict(self.js_options, **js_options).items(): 105 | if k in self.js_options_map: 106 | k = self.js_options_map[k] 107 | options[k] = v 108 | 109 | if self.ajax: 110 | ajax_url = options.pop('ajax_url', None) 111 | quiet_millis = options.pop('quietMillis', 100) 112 | is_jsonp = options.pop('jsonp', False) 113 | 114 | ajax_opts = options.get('ajax', {}) 115 | 116 | default_ajax_opts = { 117 | 'url': ajax_url or self.reverse('select2_fetch_items'), 118 | 'dataType': 'jsonp' if is_jsonp else 'json', 119 | 'quietMillis': quiet_millis, 120 | } 121 | for k, v in ajax_opts.items(): 122 | if k in self.js_options_map: 123 | k = self.js_options_map[k] 124 | default_ajax_opts[k] = v 125 | options['ajax'] = default_ajax_opts 126 | 127 | if not self.is_required: 128 | options.update({'allowClear': options.get('allowClear', True)}) 129 | 130 | if self.sortable and not self.ajax: 131 | data = [] 132 | for option_value, option_label in chain(self.choices, choices): 133 | data.append(self.option_to_data(option_value, option_label)) 134 | options['data'] = list([_f for _f in data if _f]) 135 | 136 | attrs.update({ 137 | 'data-select2-options': json.dumps(options), 138 | }) 139 | 140 | if self.ajax: 141 | attrs.update({ 142 | 'data-init-selection-url': self.reverse('select2_init_selection'), 143 | }) 144 | if self.ajax or self.sortable: 145 | self.input_type = 'hidden' 146 | return super(Select, self).render(name, value, attrs=attrs) 147 | else: 148 | return self.render_select(name, value, attrs=attrs, choices=choices) 149 | 150 | def render_select(self, name, value, attrs=None, choices=()): 151 | if value is None: 152 | value = '' 153 | attrs = attrs or {} 154 | attrs['name'] = name 155 | final_attrs = self.build_attrs(attrs) 156 | output = [u'' % flatatt(final_attrs)] 157 | if not isinstance(value, (list, tuple)): 158 | value = [value] 159 | options = self.render_options(choices, value) 160 | if options: 161 | output.append(options) 162 | output.append(u'') 163 | return mark_safe(u'\n'.join(output)) 164 | 165 | def render_option(self, selected_choices, option_value, option_label): 166 | option_value = force_str(option_value) 167 | if option_value in selected_choices: 168 | selected_html = u' selected="selected"' 169 | if not self.allow_multiple_selected: 170 | # Only allow for a single selection. 171 | selected_choices.remove(option_value) 172 | else: 173 | selected_html = '' 174 | return u'%s' % ( 175 | escape(option_value), selected_html, 176 | conditional_escape(str(option_label))) 177 | 178 | def render_options(self, choices, selected_choices): 179 | # Normalize to strings. 180 | selected_choices = set(force_str(v) for v in selected_choices) 181 | output = [] 182 | for option_value, option_label in chain(self.choices, choices): 183 | if isinstance(option_label, (list, tuple)): 184 | output.append(u'' % escape(force_str(option_value))) 185 | for option in option_label: 186 | output.append(self.render_option(selected_choices, *option)) 187 | output.append(u'') 188 | else: 189 | output.append(self.render_option(selected_choices, option_value, option_label)) 190 | return u'\n'.join(output) 191 | 192 | 193 | class SelectMultiple(Select): 194 | 195 | allow_multiple_selected = True 196 | 197 | def __init__(self, attrs=None, choices=(), js_options=None, *args, **kwargs): 198 | options = {} 199 | default_attrs = {} 200 | ajax = kwargs.get('ajax', self.ajax) 201 | sortable = kwargs.get('sortable', self.sortable) 202 | if ajax or sortable: 203 | options.update({'multiple': True}) 204 | else: 205 | default_attrs.update({ 206 | 'multiple': 'multiple', 207 | }) 208 | attrs = dict(default_attrs, **attrs) if attrs else default_attrs 209 | if js_options is not None: 210 | options.update(js_options) 211 | 212 | super(SelectMultiple, self).__init__(attrs=attrs, choices=choices, 213 | js_options=options, *args, **kwargs) 214 | 215 | def format_value(self, value): 216 | if isinstance(value, list): 217 | value = u','.join([force_str(v) for v in value]) 218 | return value 219 | 220 | if django.VERSION < (1, 10): 221 | _format_value = format_value 222 | 223 | def value_from_datadict(self, data, files, name): 224 | # Since ajax widgets use hidden or text input fields, when using ajax the value needs to be a string. 225 | if not self.ajax and not self.sortable and isinstance(data, MultiValueDict): 226 | value = data.getlist(name) 227 | else: 228 | value = data.get(name, None) 229 | if isinstance(value, str): 230 | return [v for v in value.split(',') if v] 231 | return value 232 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 3.0.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [metadata] 9 | license_file = LICENSE 10 | 11 | [flake8] 12 | exclude = tmp 13 | ignore = E722 14 | max-line-length = 100 15 | 16 | # For coverage options, see https://coverage.readthedocs.io/en/coverage-4.2/config.html 17 | [coverage:run] 18 | branch = True 19 | source = select2 20 | omit = 21 | 22 | [coverage:html] 23 | directory = build/coverage 24 | title = select2 Coverage 25 | 26 | [coverage:report] 27 | # Regexes for lines to exclude from consideration 28 | ignore_errors = True 29 | exclude_lines = 30 | # Have to re-enable the standard pragma 31 | pragma: no cover 32 | # Don't complain about missing debug-only code: 33 | def __repr__ 34 | if self\.debug 35 | # Don't complain if tests don't hit defensive assertion code: 36 | raise AssertionError 37 | raise NotImplementedError 38 | # Don't complain if non-runnable code isn't run: 39 | if __name__ == .__main__.: 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import codecs 3 | import os 4 | from setuptools import setup, find_packages 5 | 6 | readme_rst = os.path.join(os.path.dirname(__file__), 'README.rst') 7 | 8 | setup( 9 | name='django-select2-forms', 10 | version='3.0.0', 11 | description='Django form fields using the Select2 jQuery plugin', 12 | long_description=codecs.open(readme_rst, encoding='utf-8').read(), 13 | author='Frankie Dintino', 14 | author_email='fdintino@theatlantic.com', 15 | url='https://github.com/theatlantic/django-select2-forms', 16 | packages=find_packages(), 17 | license='BSD', 18 | platforms='any', 19 | install_requires=[ 20 | 'django-sortedm2m', 21 | ], 22 | classifiers=[ 23 | 'Development Status :: 5 - Production/Stable', 24 | 'Environment :: Web Environment', 25 | 'Intended Audience :: Developers', 26 | 'Operating System :: OS Independent', 27 | 'Programming Language :: Python', 28 | 'Framework :: Django', 29 | 'Framework :: Django :: 2.2', 30 | 'Framework :: Django :: 3.1', 31 | 'Framework :: Django :: 3.2', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3.7', 35 | 'Programming Language :: Python :: 3.8', 36 | 'Programming Language :: Python :: 3.9', 37 | 38 | ], 39 | include_package_data=True, 40 | zip_safe=False 41 | ) 42 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theatlantic/django-select2-forms/afa332e6848a3ff2a5df3eb461969815faefb723/tests/__init__.py -------------------------------------------------------------------------------- /tests/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Publisher, Author, Book, Library 4 | 5 | 6 | class BookInline(admin.StackedInline): 7 | model = Book 8 | extra = 0 9 | 10 | 11 | @admin.register(Library) 12 | class LibraryAdmin(admin.ModelAdmin): 13 | inlines = [BookInline] 14 | 15 | 16 | admin.site.register(Publisher) 17 | admin.site.register(Author) 18 | admin.site.register(Book) 19 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models import Q 3 | 4 | from select2.fields import ( 5 | ForeignKey as Select2ForeignKey, ManyToManyField as Select2ManyToManyField) 6 | 7 | 8 | class Publisher(models.Model): 9 | name = models.CharField(max_length=100) 10 | country = models.CharField(max_length=2) 11 | 12 | class Meta: 13 | ordering = ('name',) 14 | 15 | def __str__(self): 16 | return self.name 17 | 18 | 19 | class Author(models.Model): 20 | first_name = models.CharField(max_length=100) 21 | last_name = models.CharField(max_length=100) 22 | alive = models.BooleanField(default=True) 23 | 24 | class Meta: 25 | ordering = ('first_name', 'last_name') 26 | 27 | def __str__(self): 28 | return "%s %s" % (self.first_name, self.last_name) 29 | 30 | 31 | class Library(models.Model): 32 | name = models.CharField(max_length=100) 33 | 34 | def __str__(self): 35 | return self.name 36 | 37 | 38 | class Book(models.Model): 39 | title = models.CharField(max_length=100) 40 | library = models.ForeignKey(Library, blank=True, null=True, on_delete=models.CASCADE) 41 | 42 | publisher = Select2ForeignKey( 43 | Publisher, blank=True, null=True, related_name="+", 44 | overlay="Choose a publisher...", on_delete=models.CASCADE) 45 | 46 | us_publisher = Select2ForeignKey( 47 | Publisher, blank=True, null=True, related_name="+", 48 | verbose_name="U.S. Publisher", limit_choices_to=Q(country='US'), 49 | on_delete=models.CASCADE) 50 | 51 | publisher_ajax = Select2ForeignKey( 52 | Publisher, blank=True, null=True, related_name="+", 53 | verbose_name="Publisher (AJAX)", ajax=True, search_field='name', 54 | on_delete=models.CASCADE) 55 | 56 | us_publisher_ajax = Select2ForeignKey( 57 | Publisher, blank=True, null=True, related_name="+", 58 | verbose_name="U.S. Publisher (AJAX)", ajax=True, search_field='name', 59 | limit_choices_to=Q(country='US'), on_delete=models.CASCADE) 60 | 61 | authors_kwargs = { 62 | 'blank': True, 63 | 'sort_field': 'position', 64 | } 65 | 66 | authors = Select2ManyToManyField(Author, overlay="Choose authors...", **authors_kwargs) 67 | 68 | alive_authors = Select2ManyToManyField( 69 | Author, limit_choices_to=Q(alive=True), db_table="tests_book_alive_authors", 70 | related_name="book_set1", **authors_kwargs) 71 | authors_ajax = Select2ManyToManyField( 72 | Author, verbose_name="Authors (AJAX)", ajax=True, search_field="last_name", 73 | db_table="tests_book_authors_ajax", related_name="book_set2", 74 | **authors_kwargs) 75 | alive_authors_ajax = Select2ManyToManyField( 76 | Author, verbose_name="Alive authors (AJAX)", ajax=True, search_field="last_name", 77 | limit_choices_to=Q(alive=True), related_name="book_set3", 78 | db_table="tests_book_alive_authors_ajax", **authors_kwargs) 79 | 80 | # For testing search_field passed a callable 81 | authors_full_name_ajax = Select2ManyToManyField( 82 | Author, verbose_name="Authors (AJAX full name search)", ajax=True, 83 | search_field=lambda q: Q(first_name__icontains=q) | Q(last_name__icontains=q), 84 | db_table="tests_book_authors_full_name_ajax", related_name="book_set4", 85 | **authors_kwargs) 86 | 87 | class Meta: 88 | ordering = ('title',) 89 | 90 | def __str__(self): 91 | return self.title 92 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 3 | SECRET_KEY = 'select2-secret-key' 4 | DEBUG = True 5 | ROOT_URLCONF = 'urls' 6 | WSGI_APPLICATION = None 7 | TIME_ZONE = 'UTC' 8 | STATIC_URL = '/static/' 9 | 10 | INSTALLED_APPS = [ 11 | 'django.contrib.admin', 12 | 'django.contrib.auth', 13 | 'django.contrib.contenttypes', 14 | 'django.contrib.sessions', 15 | 'django.contrib.messages', 16 | 'django.contrib.staticfiles', 17 | 'select2', 18 | 'tests', 19 | ] 20 | 21 | MIDDLEWARE = [ 22 | 'django.middleware.security.SecurityMiddleware', 23 | 'django.contrib.sessions.middleware.SessionMiddleware', 24 | 'django.middleware.common.CommonMiddleware', 25 | 'django.middleware.csrf.CsrfViewMiddleware', 26 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 27 | 'django.contrib.messages.middleware.MessageMiddleware', 28 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 29 | ] 30 | 31 | TEMPLATES = [{ 32 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 33 | 'DIRS': [BASE_DIR], 34 | 'APP_DIRS': True, 35 | 'OPTIONS': { 36 | 'context_processors': [ 37 | 'django.template.context_processors.debug', 38 | 'django.template.context_processors.request', 39 | 'django.contrib.auth.context_processors.auth', 40 | 'django.contrib.messages.context_processors.messages', 41 | ], 42 | }, 43 | },] 44 | 45 | DATABASES = { 46 | 'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'db.sqlite'} 47 | } 48 | -------------------------------------------------------------------------------- /tests/test_admin.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import unittest 3 | 4 | import django 5 | from django.conf import settings 6 | from selenosis.testcases import AdminSelenosisTestCase 7 | from selenium.webdriver.common.keys import Keys 8 | 9 | from .models import Author, Publisher, Book, Library 10 | 11 | 12 | def load_fixtures(): 13 | Publisher.objects.bulk_create([ 14 | Publisher(pk=pk, name=name, country=country) 15 | for pk, name, country in [ 16 | [1, "University of Minnesota Press", "US"], 17 | [2, "Penguin Press", "US"], 18 | [3, "Presses Universitaires de France", "FR"], 19 | [4, "Columbia University Press", "US"], 20 | ] 21 | ]) 22 | 23 | Author.objects.bulk_create([ 24 | Author(pk=pk, first_name=first, last_name=last, alive=alive) 25 | for pk, first, last, alive in [ 26 | [1, "Gilles", "Deleuze", False], 27 | [2, "Félix", "Guattari", False], 28 | [3, "Thomas", "Pynchon", True], 29 | [4, "Mark", "Leyner", True], 30 | ] 31 | ]) 32 | 33 | 34 | class TestAdmin(AdminSelenosisTestCase): 35 | 36 | root_urlconf = "tests.urls" 37 | 38 | def setUp(self): 39 | super().setUp() 40 | load_fixtures() 41 | 42 | def initialize_page(self): 43 | super(TestAdmin, self).initialize_page() 44 | self.selenium.execute_script("""(function($) { 45 | $(document).ready(function() { 46 | window._select2_test_data = { 47 | load_count: 0, 48 | open_count: 0, 49 | change_count: 0 50 | }; 51 | $(document).on('select2-open', '.django-select2', function() { 52 | window._select2_test_data.open_count++; 53 | }); 54 | $(document).on('change', '.django-select2', function() { 55 | window._select2_test_data.change_count++; 56 | }); 57 | $(document).on('select2-loaded', '.django-select2', function() { 58 | window._select2_test_data.load_count++; 59 | }); 60 | }) 61 | })(django.jQuery)""") 62 | 63 | def field_is_active(self, field): 64 | return self.selenium.execute_script( 65 | 'return django.jQuery("#s2id_id_%s").is(".select2-container-active")' % field) 66 | 67 | def save_form(self): 68 | # Click on header to blur focus from any input elements 69 | with self.clickable_selector('#content > h1, #grp-content-title > h1') as el: 70 | el.click() 71 | super(TestAdmin, self).save_form() 72 | 73 | @contextlib.contextmanager 74 | def select2_open_dropdown(self, field_name, timeout=None): 75 | is_m2m = "authors" in field_name 76 | 77 | count_attr = 'open' if is_m2m else 'load' 78 | 79 | def get_count(): 80 | return self.selenium.execute_script( 81 | "return _select2_test_data.%s_count" % count_attr) or 0 82 | 83 | initial_count = get_count() 84 | if not self.field_is_active(field_name): 85 | click_target = '.select2-input' if is_m2m else '.select2-choice' 86 | with self.clickable_selector("#s2id_id_%s %s" % (field_name, click_target)) as el: 87 | el.click() 88 | self.wait_until( 89 | lambda d: get_count() > initial_count, 90 | timeout=timeout, 91 | message="Timeout waiting for %s of field %s" % (count_attr, field_name)) 92 | with self.clickable_selector(".select2-focused") as el: 93 | yield el 94 | 95 | def get_dropdown_count(self): 96 | return len(self.selenium.find_elements_by_css_selector( 97 | '.select2-drop-active .select2-results ' 98 | '.select2-result-selectable:not(.select2-selected)')) 99 | 100 | def select2_send_keys(self, field_name, keys, clear=False, timeout=None): 101 | count_attr = 'change' if Keys.ENTER in keys else 'load' 102 | 103 | def get_count(): 104 | return self.selenium.execute_script( 105 | "return _select2_test_data.%s_count" % count_attr) or 0 106 | 107 | with self.select2_open_dropdown(field_name) as el: 108 | if clear: 109 | el.clear() 110 | initial_count = get_count() 111 | if keys: 112 | el.send_keys(keys) 113 | self.wait_until( 114 | lambda d: get_count() > initial_count, 115 | timeout=timeout, 116 | message="Timeout waiting for %s of field %s" % (count_attr, field_name)) 117 | return el 118 | 119 | def test_fk(self): 120 | book = Book.objects.create(title="Difference and Repetition") 121 | self.load_admin(book) 122 | columbia_univ_press = Publisher.objects.get(name='Columbia University Press') 123 | self.select2_send_keys('publisher', '') 124 | self.assertEqual(self.get_dropdown_count(), 5) 125 | self.select2_send_keys('publisher', 'co') 126 | self.assertEqual(self.get_dropdown_count(), 1) 127 | self.select2_send_keys('publisher', Keys.ENTER) 128 | self.save_form() 129 | book.refresh_from_db() 130 | self.assertEqual(book.publisher, columbia_univ_press) 131 | 132 | def test_fk_limit(self): 133 | book = Book.objects.create(title="Difference and Repetition") 134 | self.load_admin(book) 135 | columbia_univ_press = Publisher.objects.get(name='Columbia University Press') 136 | self.select2_send_keys('us_publisher', '') 137 | self.assertEqual(self.get_dropdown_count(), 4) 138 | self.select2_send_keys('us_publisher', 'co') 139 | self.assertEqual(self.get_dropdown_count(), 1) 140 | self.select2_send_keys('us_publisher', Keys.ENTER) 141 | self.save_form() 142 | book.refresh_from_db() 143 | self.assertEqual(book.us_publisher, columbia_univ_press) 144 | 145 | def test_fk_ajax(self): 146 | book = Book.objects.create(title="Difference and Repetition") 147 | self.load_admin(book) 148 | columbia_univ_press = Publisher.objects.get(name='Columbia University Press') 149 | self.select2_send_keys('publisher_ajax', '') 150 | self.assertEqual(self.get_dropdown_count(), 4) 151 | self.select2_send_keys('publisher_ajax', 'co') 152 | self.assertEqual(self.get_dropdown_count(), 1) 153 | self.select2_send_keys('publisher_ajax', Keys.ENTER) 154 | self.save_form() 155 | book.refresh_from_db() 156 | self.assertEqual(book.publisher_ajax, columbia_univ_press) 157 | 158 | def test_fk_limit_ajax(self): 159 | book = Book.objects.create(title="Difference and Repetition") 160 | self.load_admin(book) 161 | columbia_univ_press = Publisher.objects.get(name='Columbia University Press') 162 | self.select2_send_keys('us_publisher_ajax', '') 163 | self.assertEqual(self.get_dropdown_count(), 3) 164 | self.select2_send_keys('us_publisher_ajax', 'co') 165 | self.assertEqual(self.get_dropdown_count(), 1) 166 | self.select2_send_keys('us_publisher_ajax', Keys.ENTER) 167 | self.save_form() 168 | book.refresh_from_db() 169 | self.assertEqual(book.us_publisher_ajax, columbia_univ_press) 170 | 171 | def test_m2m(self): 172 | book = Book.objects.create(title="A Thousand Plateaus") 173 | self.load_admin(book) 174 | self.select2_send_keys('authors', '') 175 | self.assertEqual(self.get_dropdown_count(), 4) 176 | self.select2_send_keys('authors', 'l') 177 | self.assertEqual(self.get_dropdown_count(), 3) 178 | self.select2_send_keys('authors', 'euze') 179 | self.assertEqual(self.get_dropdown_count(), 1) 180 | self.select2_send_keys('authors', Keys.ENTER) 181 | self.select2_send_keys('authors', " %s" % Keys.BACKSPACE) 182 | self.assertEqual(self.get_dropdown_count(), 3) 183 | self.select2_send_keys('authors', 'guat') 184 | self.assertEqual(self.get_dropdown_count(), 1) 185 | self.select2_send_keys('authors', Keys.ENTER) 186 | self.save_form() 187 | book.refresh_from_db() 188 | deleuze = Author.objects.get(last_name='Deleuze') 189 | guattari = Author.objects.get(last_name='Guattari') 190 | self.assertEqual(list(book.authors.all()), [deleuze, guattari]) 191 | 192 | def test_m2m_ajax(self): 193 | book = Book.objects.create(title="A Thousand Plateaus") 194 | self.load_admin(book) 195 | self.select2_send_keys('authors_ajax', '') 196 | self.assertEqual(self.get_dropdown_count(), 4) 197 | self.select2_send_keys('authors_ajax', 'l') 198 | self.assertEqual(self.get_dropdown_count(), 2) 199 | self.select2_send_keys('authors_ajax', 'euze') 200 | self.assertEqual(self.get_dropdown_count(), 1) 201 | self.select2_send_keys('authors_ajax', Keys.ENTER) 202 | self.select2_send_keys('authors_ajax', " %s" % Keys.BACKSPACE) 203 | self.assertEqual(self.get_dropdown_count(), 3) 204 | self.select2_send_keys('authors_ajax', 'guat') 205 | self.assertEqual(self.get_dropdown_count(), 1) 206 | self.select2_send_keys('authors_ajax', Keys.ENTER) 207 | self.save_form() 208 | book.refresh_from_db() 209 | deleuze = Author.objects.get(last_name='Deleuze') 210 | guattari = Author.objects.get(last_name='Guattari') 211 | self.assertEqual(list(book.authors_ajax.all()), [deleuze, guattari]) 212 | 213 | def test_m2m_limit(self): 214 | book = Book.objects.create(title="Against the Day") 215 | self.load_admin(book) 216 | self.select2_send_keys('alive_authors', '') 217 | self.assertEqual(self.get_dropdown_count(), 2) 218 | self.select2_send_keys('alive_authors', 'pync') 219 | self.assertEqual(self.get_dropdown_count(), 1) 220 | self.select2_send_keys('alive_authors', Keys.ENTER) 221 | self.save_form() 222 | book.refresh_from_db() 223 | pynchon = Author.objects.get(last_name='Pynchon') 224 | self.assertEqual(list(book.alive_authors.all()), [pynchon]) 225 | 226 | def test_m2m_limit_ajax(self): 227 | book = Book.objects.create(title="Against the Day") 228 | self.load_admin(book) 229 | self.select2_send_keys('alive_authors_ajax', '') 230 | self.assertEqual(self.get_dropdown_count(), 2) 231 | self.select2_send_keys('alive_authors_ajax', 'pync') 232 | self.assertEqual(self.get_dropdown_count(), 1) 233 | self.select2_send_keys('alive_authors_ajax', Keys.ENTER) 234 | self.save_form() 235 | book.refresh_from_db() 236 | pynchon = Author.objects.get(last_name='Pynchon') 237 | self.assertEqual(list(book.alive_authors_ajax.all()), [pynchon]) 238 | 239 | def test_m2m_ajax_custom_search_field(self): 240 | book = Book.objects.create(title="A Thousand Plateaus") 241 | self.load_admin(book) 242 | self.select2_send_keys('authors_full_name_ajax', '') 243 | self.assertEqual(self.get_dropdown_count(), 4) 244 | self.select2_send_keys('authors_full_name_ajax', 'l') 245 | self.assertEqual(self.get_dropdown_count(), 3) 246 | self.select2_send_keys('authors_full_name_ajax', 'euze') 247 | self.assertEqual(self.get_dropdown_count(), 1) 248 | self.select2_send_keys('authors_full_name_ajax', Keys.ENTER) 249 | self.select2_send_keys('authors_full_name_ajax', " %s" % Keys.BACKSPACE) 250 | self.assertEqual(self.get_dropdown_count(), 3) 251 | self.select2_send_keys('authors_full_name_ajax', 'guat') 252 | self.assertEqual(self.get_dropdown_count(), 1) 253 | self.select2_send_keys('authors_full_name_ajax', Keys.ENTER) 254 | self.save_form() 255 | book.refresh_from_db() 256 | deleuze = Author.objects.get(last_name='Deleuze') 257 | guattari = Author.objects.get(last_name='Guattari') 258 | self.assertEqual(list(book.authors_full_name_ajax.all()), [deleuze, guattari]) 259 | 260 | def test_inline_add_init(self): 261 | if django.VERSION < (1, 9): 262 | raise unittest.SkipTest("Django 1.8 does not have the formset:added event") 263 | if 'grappelli' in settings.INSTALLED_APPS: 264 | raise unittest.SkipTest("django-grappelli does not have the formset:added event") 265 | library = Library.objects.create(name="Princeton University Library") 266 | columbia_univ_press = Publisher.objects.get(name='Columbia University Press') 267 | self.load_admin(library) 268 | with self.clickable_selector(".add-row a") as el: 269 | el.click() 270 | with self.clickable_selector('#id_book_set-0-title') as el: 271 | el.send_keys('Difference and Repetition') 272 | self.select2_send_keys('book_set-0-publisher', u'co%s' % Keys.ENTER) 273 | self.save_form() 274 | library.refresh_from_db() 275 | books = library.book_set.all() 276 | self.assertNotEqual(len(books), 0, "Book inline did not save") 277 | self.assertEqual(books[0].publisher, columbia_univ_press) 278 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.urls import include, re_path 3 | from django.contrib import admin 4 | 5 | 6 | admin.autodiscover() 7 | 8 | urlpatterns = [ 9 | re_path(r"^select2/", include("select2.urls")), 10 | re_path(r'^admin/', admin.site.urls) 11 | ] 12 | 13 | try: 14 | import grappelli.urls 15 | except ImportError: 16 | pass 17 | else: 18 | urlpatterns += [re_path(r"^grappelli/", include(grappelli.urls))] 19 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py3{7,8,9}-django{22,31,32} 4 | 5 | [pytest] 6 | django_find_project = false 7 | DJANGO_SETTINGS_MODULE=tests.settings 8 | 9 | [testenv] 10 | description = Run tests in {envname} environment 11 | setenv = 12 | PYTHONPATH = {toxinidir}:{env:PYTHONPATH:} 13 | commands = pytest {posargs} 14 | deps = 15 | pytest>=5.2.0 16 | pytest-django 17 | selenium==3.141.0 18 | django-selenosis 19 | django22: Django>=2.2,<3.0 20 | django31: Django>=3.1,<3.2 21 | django32: Django>=3.2,<4.0 22 | django22-grp: django-grappelli==2.13.4 23 | django31-grp: django-grappelli==2.14.4 24 | django32-grp: django-grappelli==2.15.1 25 | 26 | [testenv:clean] 27 | description = Clean all build and test artifacts 28 | skipsdist = true 29 | skip_install = true 30 | deps = 31 | whitelist_externals = 32 | find 33 | rm 34 | commands = 35 | find {toxinidir} -type f -name "*.pyc" -delete 36 | find {toxinidir} -type d -name "__pycache__" -delete 37 | rm -f {toxinidir}/tests/db.sqlite {toxworkdir} {toxinidir}/.pytest_cache {toxinidir}/build 38 | 39 | [testenv:docs] 40 | description = Build Sphinx documentation 41 | skipsdist = true 42 | skip_install = true 43 | commands = 44 | sphinx-build -b html docs/source docs 45 | deps = 46 | sphinx 47 | sphinx_rtd_theme 48 | 49 | [testenv:pep8] 50 | description = Run PEP8 flake8 against the select2/ package directory 51 | skipsdist = true 52 | skip_install = true 53 | basepython = python3.7 54 | deps = flake8 55 | commands = flake8 select2 tests 56 | 57 | [testenv:coverage] 58 | description = Run test coverage and display results 59 | deps = 60 | {[testenv]deps} 61 | coverage 62 | pytest-cov 63 | whitelist_externals = 64 | echo 65 | commands = 66 | pytest --cov-config .coveragerc --cov-report html --cov-report term --cov=select2 67 | echo HTML coverage report: {toxinidir}/build/coverage/index.html 68 | 69 | [gh-actions] 70 | python = 71 | 3.7: py37 72 | 3.8: py38 73 | 3.9: py39 74 | --------------------------------------------------------------------------------
"),s.addClass("select2-results-dept-"+j),s.addClass("select2-result"),s.addClass(q?"select2-result-selectable":"select2-result-unselectable"),r&&s.addClass("select2-disabled"),t&&s.addClass("select2-result-with-children"),s.addClass(g.opts.formatResultCssClass(m)),q=e(document.createElement("div")),q.addClass("select2-result-label"),r=a.formatResult(m,q,d,g.opts.escapeMarkup),r!== 507 | h&&q.html(r),s.append(q),t&&(t=e("