diff --git a/.gitignore b/.gitignore
index aa970da6..2f0efe8f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+.idea
node_modules
dist/js/i18n/build.txt
.sass-cache
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index 71f68fb0..00000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,321 +0,0 @@
-# Change Log
-
-## 4.0.7
-
-### New features/improvements
-- Do not close on select if Ctrl or Meta (Cmd) keys being held (#5222)
-
-### Bug fixes
-- Fixed issue where single select boxes would automatically reopen when they were closed (#5490, #5492)
-
-### Miscellaneous
-- Move almost and jquery-mousewheel to devDependencies (#5489)
-
-## 4.0.6
-
-### New features/improvements
-- Add style property to package.json (#5019)
-- Implement `clear` and `clearing` events (#5058)
-- Add `scrollAfterSelect` option (#5150)
-- Add missing diacritics (#4118, #4337, #5464)
-
-### Bug fixes
-- Fix up arrow error when there are no options in dropdown (#5127)
-- Add `;` before beginning of factory wrapper (#5089)
-- Fix IE11 issue with select losing focus after selecting an item (#4860)
-- Clear tooltip from `select2-selection__rendered` when selection is cleared (#4640, #4746)
-- Fix keyboard not closing when closing dropdown on iOS 10 (#4680)
-- User-defined types not normalized properly when passed in as data (#4632)
-- Perform deep merge for `Defaults.set()` (#4364)
-- Fix "the results could not be loaded" displaying during AJAX request (#4356)
-- Cache objects in `Utils.__cache` instead of using `$.data` (#4346, #5486)
-- Removing the double event binding registration of `selection:update` (#4306)
-
-#### Accessibility
-- Improve `.select2-hidden-accessible` (#4908)
-- Add role and aria-readonly attributes to single selection dropdown value (#4881)
-
-### Translations
-- Add Turkmen translations (`tk`) (#5125)
-- Fix error in French translations (#5122)
-- Add Albanian translation (`sq`) (#5199)
-- Add Georgian translation (`ka`) (#5179)
-- Add Nepali translation (`ne`) (#5295)
-- Add Bangla translation (`bn`) (#5248)
-- Add `removeAllItems` translation for clear "x" title (#5291)
-- Fix wording in Vietnamese translations (#5387)
-- Fix error in Russian translation (#5401)
-
-### Miscellaneous
-- Remove duplicate CSS selector in classic theme (#5115)
-
-## 4.0.5
-
-### Bug fixes
-- Replace `autocapitalize=off` with `autocapitalize=none` (#4994)
-
-### Translations
-- Vietnamese: remove an unnecessary quote mark (#5059)
-- Czech: Add missing commas and periods (#5052)
-- Spanish: Update the 'errorLoading' message (#5032)
-- Fix typo in Romanian (#5005)
-- Improve French translation (#4988)
-- Add Pashto translation (`ps`) (#4960)
-- Add translations for lower and upper Sorbian (`dsb` and `hsb`) (#4949)
-- Updates to Slovak (#4915)
-- Fixed Norwegian `inputTooShort` message (#4817, 4896)
-- Add Afrikaans translation (`af`) (#4850)
-- Add Bosnian translation (`bs`) (#4504)
-
-## 4.0.4
-
-### New features / Improvements
-- Make tag matching case insensitive [https://github.com/select2/select2/commit/cb9a90457867ffb14c7b1550bb67e872e0a5c2dd, https://github.com/select2/select2/commit/1167bace78cd3b1a918c1b04f3bac54674eab62b]
-- Support selecting options with blank or `0` option values [https://github.com/select2/select2/commit/16b4840c0e2df0461998e3b464ee0a546173950d, https://github.com/select2/select2/commit/0358ee528765157234643d289bce6b8ca5889c72]
-
-### Bug fixes
-- Fix issue with entire form losing focus when tabbing away from a Select2 control (#4419)
-- Fix UMD support for CommonJS [https://github.com/select2/select2/commit/45a877345482956021161203ac789c25f40a7d5e]
-
-### Documentation
-- Github Pages documentation has been deprecated, replaced with https://github.com/select2/docs
-- Add django-autocomplete-light to integrations [https://github.com/select2/select2/pull/4597]
-- Correct typo in options page [https://github.com/select2/select2/pull/4389]
-- Correct misspelling in AJAX query parameters example [https://github.com/select2/select2/pull/4435]
-- "highlight" should be "focus" in focus example [https://github.com/select2/select2/pull/4441]
-- Correct misspelling in `` serialization example [https://github.com/select2/select2/pull/4538]
-- Correct typos in documentation [https://github.com/select2/select2/pull/4663]
-
-### Translations
-- Add `errorLoading` Hungarian translation [https://github.com/select2/select2/commit/7d1d13352321e21670ff1c6cba7413aa264fd57a]
-- Add `errorLoading` German translation [https://github.com/select2/select2/commit/4df965219ea4c39147fde9335bc260840465933a]
-- Add Slovene language [https://github.com/select2/select2/commit/8e6422c570a87da8d89c45daf0d253695a943c84]
-- Add `errorLoading` Galician translation [https://github.com/select2/select2/commit/8fcc6202c37f4e06d951342bf142a3b906b6b8e3]
-- Add `errorLoading` Thai translation [https://github.com/select2/select2/commit/625fc78ee616baedf64aa37357403b4b72c7363c]
-- Add `searching` and `errorLoading` Finnish translations [https://github.com/select2/select2/pull/4730]
-- Add `errorLoading` Turkish translation [https://github.com/select2/select2/commit/fd4a0825315c7055347726d5818c999279f96ff8, https://github.com/select2/select2/commit/751b36767f9f28b9de9428d5e8035c9a404915d9]
-- Add Armenian language [https://github.com/select2/select2/commit/f6fa52dcc02341df1523f50348f2effc54ee2911]
-
-## 4.0.3
-
-This is the third bugfix release of Select2 4.0.0. It builds upon the [second bugfix release](https://github.com/select2/select2/releases/tag/4.0.2) and fixes many common issues.
-
-### New features / Improvements
-- The old `dropdownAutoWidth` option now properly works [https://github.com/select2/select2/commit/fe26b083eb830836061de1458e483782cefef424]
-- A `focus` event on the original `` is now handled [https://github.com/select2/select2/commit/31e7a1d4c52ed7477769fcad5d15166ae3c9b4d0]
-- Adding and removing options now refreshes the selection automatically [https://github.com/select2/select2/commit/ea79a197e0ffe55aa600eed6d18cbd1c804c3176]
-
-### Bug fixes
-- `select2('option')` no longer mutate the arguments when working on multiple elements [https://github.com/select2/select2/commit/c2c1aeef31c95c6df5545c900a4e1782d712497c]
-- Better detect aborted requests [https://github.com/select2/select2/commit/cfb66f5e4f71a56c46a6890c5dde4b7f24f11fa8]
-- New options are now properly created during tokenization [https://github.com/select2/select2/commit/3b8cd2e36990e695e4cb4b966c8658e7ca1574dc]
-- Fix positioning bug with non-static parents for the dropdown [https://github.com/select2/select2/pull/4267]
-- Infinite scrolling no longer resets the keyboard focus [https://github.com/select2/select2/commit/e897d008a672da262ba84cee2a144578696ada29, https://github.com/select2/select2/commit/9f581285d88128b29a01fc1e5fd2d445d610b553]
-- `selectOnClose` now works properly with `closeOnSelect` [https://github.com/select2/select2/commit/481c43883e23874e9c35879d173eb8cc5b994b12]
-- Apply `ajax.delay` to empty search terms as well [https://github.com/select2/select2/commit/4b9e02f02211248be25ac4c16d4635cf38237bb9]
-
-### Documentation
-- Added example for attaching event listeners [https://github.com/select2/select2/commit/84d6b5d840f7f4e6b7a2fb3f08424bf5495c876d]
-- Correct link to the [Select2 Bootstrap Theme](https://github.com/select2/select2-bootstrap-theme) [https://github.com/select2/select2/pull/4318]
-- Added example for using a `` [https://github.com/select2/select2/commit/3bc7f4ac78b58eff8cd17b3273596638c3c9c5c1]
-- Add documentation for `ajax.url` [https://github.com/select2/select2/commit/5a831afb9a7d46e8f20aec21164cfbfd182024de]
-- Added favicon [https://github.com/select2/select2/pull/4379]
-
-### Translations
-- Add Khmer translation [https://github.com/select2/select2/pull/4246]
-- Added Norwegian bokmaal for `errorLoading` [https://github.com/select2/select2/pull/4259]
-- Fixed pluralization in Lithuanian translation [https://github.com/select2/select2/commit/5b5eddd183c87bf43165b3a98e03eabe10e9fa58]
-- Add French translation for `errorLoading` [https://github.com/select2/select2/commit/b1ea28bb7d8c02b3b352f558031ccfc8041122eb]
-- Add Greek translation [https://github.com/select2/select2/pull/4139]
-
-## 4.0.2
-
-This is the second bugfix release of Select2 4.0.0. It builds upon the [first release candidate of Select2 4.0.2](https://github.com/select2/select2/releases/tag/4.0.2-rc.1) with some minor improvements.
-
-### New features / Improvements
-
-- Added `insertTag` option to control the placement of the `tags` option [https://github.com/select2/select2/pull/4008]
-- Added handler for AJAX errors [https://github.com/select2/select2/issues/3501]
-- Added insertTag to control the tag position [https://github.com/select2/select2/pull/4008]
-
-### Bug fixes
-
-- Fixed positioning issues with static dropdown parents [https://github.com/select2/select2/issues/3970]
-- Fixed existing selections not always being respected with array data [https://github.com/select2/select2/issues/3990]
-- Sanitize automatically generated ids so CSS identifiers can be used [https://github.com/select2/select2/issues/3618]
-- Recursively apply defaults so AJAX defaults can be set [https://github.com/select2/select2/commit/983cd8e765c5345bfe7d3bdcc3b0c882a35461ca]
-- No need to recalculate the top of the dropdown twice [https://github.com/select2/select2/pull/4155]
-
-### Documentation
-
-- Updated Bootstrap and Font Awesome dependencies [https://github.com/select2/select2/commit/a5e539b509778eabeb8ce79e191b3ee1e81f6deb, https://github.com/select2/select2/commit/81a4a68b113e0d3e0fb1d0f8b1c33ae1b48ba04f, https://github.com/select2/select2/commit/6369f5f173fb81ec692213782945cc737e248da5]
-- Use Jekyll's highlighting instead of prettify [https://github.com/select2/select2/commit/54441e6a22be3969dd934ccb769f5d7dde684bfb, https://github.com/select2/select2/commit/74387b98632c75b06d15d83ad5359b9daf0f5dcb, https://github.com/select2/select2/commit/a126b53b4c90fac33b5d855894647cd8bcac3558, https://github.com/select2/select2/commit/75163d67cb80e4279965a97e9eeda5b171806085]
-- Corrected responsive width example to properly show it working [https://github.com/select2/select2/commit/63d531a9c0ab51f05327492a56f3245777762b45]
-- Replaced protocol-relative URLs with HTTPS protocol [https://github.com/select2/select2/pull/4127]
-- Code snippets for mapping `id` and `text` [https://github.com/select2/select2/issues/4086]
-- Document how to trigger `change` just for Select2 [https://github.com/select2/select2/issues/3620]
-- Added notes about DOM events [https://github.com/select2/select2/commit/37dbe059fce4578b46b7561e6243b7fdc63ac002]
-
-### Translations
-- Correct Romanian translation [https://github.com/select2/select2/commit/72d905f9e026d49e7c600f37a1ce742c404654d7]
-
-## 4.0.1
-
-This is the first bugfix release of Select2 4.0.0. It builds upon the [first release candidate of Select2 4.0.1](https://github.com/select2/select2/releases/tag/4.0.1-rc.1) with some minor improvements.
-
-### New features / improvements
-- The option container is now passed in as the second argument when templating selections using `templateResult` [https://github.com/select2/select2/commit/dc516e7073605723be59bc727b96a3b3dea1ae5a]
-- The option container is now passed in as the second argument when templating selections using `templateSelection` [https://github.com/select2/select2/pull/3324]
-- You can immediately start typing to search when tabbing into a multiple select [https://github.com/select2/select2/commit/02cca7baa7b78e73cdcf393172ee3a54be387167, https://github.com/select2/select2/commit/79cdcc0956e242c1ce642bbaa93e538c54f4be0]
-- All parameters passed in for AJAX requests are now set as query string parameters by default [https://github.com/select2/select2/issues/3548]
-
-### Bug fixes
-- The search box will now be properly sized after removing a selection [https://github.com/select2/select2/commit/5f80c5d9f81f3c5398c3e6e3e84fd6c67c8873f1]
-- Dropdown results will now be spoken by screen readers [https://github.com/select2/select2/commit/9fae3d74e373fc646da4e39a0c2ab11efa808c3f]
-- Options are now properly cloned when initializing multiple instances at once [https://github.com/select2/select2/commit/3c8366e8769233a6b20ade934fe629279e7be6ff]
-- `selectOnClose` and now be used with `closeOnSelect` without getting a stack overflow [https://github.com/select2/select2/commit/393ca4cf7f7f7097d3a994bda3dbf195e945eba1]
-- Fixed positioning with non-static parents [https://github.com/select2/select2/commit/c9216b4b966653dd63a67e815b47899ef5325298]
-- Fixed bug where multiple selects with placeholders were buggy in IE [https://github.com/select2/select2/issues/3300]
-- Fixed bug where AJAX selects could not be initialized with array data [https://github.com/select2/select2/pull/3375]
-- `:all:` is now correctly removed when used in `containerCss` and `dropdownCss` options [https://github.com/select2/select2/pull/3464]
-- Fixed bug where the multiple select search box would appear on the left in RTL mode [https://github.com/select2/select2/pull/3502]
-- Change ALT + UP to close the dropdown instead of opening it [https://github.com/select2/select2/commit/d2346cc33186c2a00fa2dad29e8e559c42bfea00]
-- Fix focus issue with the multiple select search box when the `change` event was triggered [https://github.com/select2/select2/commit/698fe7b9e187e182f679aa679eb8b0ecb64a846b, https://github.com/select2/select2/commit/88503d2c67dc7f4fb9395a17f17edfe4948cf738, https://github.com/select2/select2/commit/dd2990adead92593a2dffff6ae004ea8b647d130]
-- Fix bug in `ArrayAdapter` where the existing `` data would be used instead of the array data [https://github.com/select2/select2/pull/3565]
-- Remove random call to `$dropdownContainer.width()` in the `AttachBody` decorator [https://github.com/select2/select2/pull/3654]
-- Fix memory leak in `AttachBody` decorator [https://github.com/select2/select2/commit/671f5a2ce21005090e0b69059799cd3dd1fbbf84]
-- Selections can no longer be removed when Select2 is in a disabled state [https://github.com/select2/select2/commit/68d068f1d2c7722d011d285a291d1f974bf09772, https://github.com/select2/select2/commit/7d8f86cbf85ebd2179195ff6a2a7a1c5dcb9da58]
-- Remove redundant `open` event trigger [https://github.com/select2/select2/pull/3507]
-- Correct references to `this` in `ajax.data` and `ajax.url` callback functions [https://github.com/select2/select2/issues/3361]
-- Apply select2('option') calls on all elements [https://github.com/select2/select2/pull/3495]
-
-### Design
-
-- Fixed original `` not always being hidden correctly in some cases [https://github.com/select2/select2/pull/3301]
-- Fix potential issue with Bootstrap's gradients in Internet Explorer [https://github.com/select2/select2/pull/3307]
-- Improve compatibility with Zurb Foundation [https://github.com/select2/select2/pull/3290]
-- Remove padding on mobile safari search field in multiple selects [https://github.com/select2/select2/pull/3605]
-- Fix the clear button appearing beneath long text [https://github.com/select2/select2/issues/3306]
-- Migrate the CSS classes for the "Loading more results" message to BEM [https://github.com/select2/select2/issues/3889]
-- Fix inline search not displaying properly in Safari [https://github.com/select2/select2/issues/3459]
-
-### Documentation
-
-- New documentation theme designed by @fk [https://github.com/select2/select2/pull/3376, https://github.com/select2/select2/pull/3467, https://github.com/select2/select2/pull/3488]
-- Update ajax example to reflect pagination [https://github.com/select2/select2/pull/3357]
-- Fix incorrect option name in `maxiumSelectionLength` example [https://github.com/select2/select2/pull/3454]
-- Fix typos in the disabled mode/results examples [https://github.com/select2/select2/pull/3665]
-- Fix `Option` parameters in the 4.0 announcement [https://github.com/select2/select2/pull/3547]
-- Fix invalid JSON in the tags example within the 4.0 announcement [https://github.com/select2/select2/pull/3637]
-
-### Translations
-- Added Cyrillic variant of the Serbian language [https://github.com/select2/select2/pull/3943]
-- Corrected Thai "no results found" translation [https://github.com/select2/select2/pull/3782]
-- Swapped the `inputTooLong` and `inputTooShort` messages in the Galician translation [https://github.com/select2/select2/pull/3291]
-- Fix improper grammar in Dutch translation [https://github.com/select2/select2/pull/3692]
-- Add Japanese translation [https://github.com/select2/select2/pull/3477]
-- Polish translation: Fixed typo in maximum selected message [https://github.com/select2/select2/pull/3587]
-- Add Malay translation [https://github.com/select2/select2/pull/3635]
-- Add `errorLoading` for Indonesian translation [https://github.com/select2/select2/pull/3635]
-- Correct grammar issues in Hebrew translation [https://github.com/select2/select2/pull/3911]
-- Add `errorLoading` for Danish translation [https://github.com/select2/select2/pull/3870]
-- Add Arabic translation [https://github.com/select2/select2/pull/3859]
-
-## 4.0.0
-
-
-This builds upon [the second release candidate](https://github.com/select2/select2/tree/4.0.0-rc.2), **so review all previous release notes** before upgrading from previous versions of Select2.
-
-### Supported environments
-- jQuery 1.7.2+
-- Modern browsers (Chrome, Firefox, Safari)
-- Internet Explorer 8+
-
-### New features
-- Fully compatible with AMD and UMD based loaders.
-- Advanced plugin system that [uses custom adapters](https://select2.org/advanced/adapters-and-decorators).
-- Full support for `jQuery.noConflict`.
-- A `` is the recommended element and [can be used for all options](https://select2.org/upgrading/migrating-from-35#no-more-hidden-input-tags). There is limited backwards-compatible support for the ` ` element in [full builds](https://select2.org/getting-started/builds-and-modules).
-- [Declarative configuration through `data-*` attributes](https://select2.org/configuration/data-attributes)
-- Easy to configure theme system and new default theme
-- You can use more specific locales (like `en-US`) and Select2 will be able to determine what translation files to load.
-
-### Breaking changes
-- Select2 now uses the MIT license
-- [The full build](https://select2.org/getting-started/builds-and-modules) of Select2 no longer includes jQuery - You must include jQuery separately on your page.
-- Select2 will prevent the inner scrolling of modals (and other scrollable containers) when it is open to prevent the UI from breaking. [Read more at the commit.](https://github.com/select2/select2/commit/003d6053a9fff587c688008397e7d5824463fe99)
-- jQuery is no longer listed as a dependency in the `bower.json`/`component.json` files.
-- [`` has replaced ` `](https://select2.org/upgrading/migrating-from-35#no-more-hidden-input-tags) for **all options** (_including remote data_)
-- The [`matcher` has been revamped](https://select2.org/upgrading/migrating-from-35#advanced-matching-of-searches) to include full context, a compatibility module (`select2/compat/matcher`) has been created
-- The [display always reflects the order](https://select2.org/upgrading/migrating-from-35#display-reflects-the-actual-order-of-the-values) data is sent to the server
-- The click mask is no longer the default (again). You can get back the old functionality by wrapping your `selectionAdapter` with the `ClickMask` (`select2/selection/clickMask`) decorator.
-- Select2 no longer stops the propagation of events happening within the dropdown and selection. You can use the `StopPropagation` modules available in the [full builds](https://select2.org/getting-started/builds-and-modules) to prevent this. [https://github.com/select2/select2/commit/8f8140e3b00c5d5bb232455137c4c633d7da4275]
-- The enter key no longer toggles the state of multiple select items in the results, but instead will only select them. Use CTRL + Space instead to toggle the state. [https://github.com/select2/select2/commit/017c20109471fa5b835603faf5dc37f7c2c2ea45]
-- Warnings will now be triggered in the developer console if Select2 detects an unsupported configuration.
-
-#### Options
-
-- The default value of the `width` option has been changed from `style` to `resolve`.
-- The `copy` value for the `width` option has been renamed to `style`.
-
-##### Renamed
-- `formatSelection` -> `templateSelection`
-- `formatResult` -> `templateResult`
-- `sortResults` -> `sorter`
-- `createSearchChoice` -> `createTag`
-- `selectOnBlur` -> `selectOnClose`
-- `ajax.jsonpCallback` -> `ajax.jsonp`
-- `ajax.results` -> `ajax.processResults`
-- `tags: [array,of,data]` -> `data: [array,of,data], tags: true`
-- `placeholderOption` has been replaced by `placeholder.id` (`placeholder` -> `placeholder.text`)
-
-##### [Internationalization](https://select2.org/i18n)
-- `formatNoMatches` -> `language.noMatches`
-- `formatSearching` -> `language.searching`
-- `formatInputTooShort` -> `language.inputTooShort`
-- `formatInputTooLong` -> `language.inputTooLong`
-- `formatAjaxError` -> `language.errorLoading`
-- `formatLoading` -> `language.loadingMore`
-- `formatSelectionTooBig` -> `language.maximumSelected`
-
-##### Deprecated/Removed
-- `initSelection` - This is [no longer needed](https://select2.org/upgrading/migrating-from-35#removed-the-requirement-of-initselection) with `` tags. Limited backwards compatibility in the [full build](https://select2.org/getting-started/builds-and-modules).
-- `id` - Data objects should now always have `id` and `text` attributes that are strings, use [`$.map`](https://api.jquery.com/jquery.map/) when migrating
-- `query` - Use a [custom data adapter](https://select2.org/upgrading/migrating-from-35#custom-data-adapters-instead-of-query) instead. Limited backwards compatibility in the [full build](https://select2.org/getting-started/builds-and-modules).
-- `ajax.params` - All parameters passed to `ajax` will be passed to the AJAX data transport function
-
-#### Methods
-
-##### Renamed
-- `.select2("val", [value])` -> `.val([value])`
-- `.select2("enable", !disabled)` -> `.prop("disabled", disabled)`
-
-##### Removed
-- `.select2("onSortStart")` and `.select2("onSortEnd")` - A custom [selection adapter](https://select2.org/advanced/default-adapters/selection) should be created instead
-- `.select2("data", data)` - Create the `` tags for the objects that you would like to set, and set the `.val` to select them
-- `.select2("readonly")` - There is [no way to make a `` element read-only](http://stackoverflow.com/q/368813/359284), disable it instead
-
-#### Events
-
-##### New
-- `select2:closing` is triggered before the dropdown is closed
-- `select2:select` is triggered when an option is selected
-
-##### Renamed
-- `select2-close` is now `select2:close`
-- `select2-open` is now `select2:open`
-- `select2-opening` is now `select2:opening`
-- `select2-selecting` is now `select2:selecting`
-- `select2-removed` is now `select2:unselect`
-- `select2-removing` is now `select2:unselecting`
-
-##### Removed
-- `select2-clearing` has been removed in favor of `select2:unselecting`
-- `select2-highlight`
-- `select2-loaded`
-- `select2-focus` - Use the native `focus` event instead
-- `select2-blur` - Use the native `blur` event instead
-- All extra properties from the `change` event were removed
- - `val` can be retrieved with `$element.val()` instead
- - `added` can be retrieved by listening to `select2:select`
- - `removed` can be retrieved by listening to `select2:unselect`
diff --git a/Gruntfile.js b/Gruntfile.js
deleted file mode 100644
index 2cefb849..00000000
--- a/Gruntfile.js
+++ /dev/null
@@ -1,260 +0,0 @@
-module.exports = function (grunt) {
- // Full list of files that must be included by RequireJS
- includes = [
- 'jquery.select2',
- 'almond',
-
- 'jquery-mousewheel' // shimmed for non-full builds
- ];
-
- fullIncludes = [
- 'jquery',
-
- 'select2/compat/containerCss',
- 'select2/compat/dropdownCss',
-
- 'select2/compat/initSelection',
- 'select2/compat/inputData',
- 'select2/compat/matcher',
- 'select2/compat/query',
-
- 'select2/dropdown/attachContainer',
- 'select2/dropdown/stopPropagation',
-
- 'select2/selection/stopPropagation'
- ].concat(includes);
-
- var i18nModules = [];
- var i18nPaths = {};
-
- var i18nFiles = grunt.file.expand({
- cwd: 'src/js'
- }, 'select2/i18n/*.js');
-
- var testFiles = grunt.file.expand('tests/**/*.html');
- var testUrls = testFiles.map(function (filePath) {
- return 'http://localhost:9999/' + filePath;
- });
-
- var testBuildNumber = "unknown";
-
- if (process.env.TRAVIS_JOB_ID) {
- testBuildNumber = "travis-" + process.env.TRAVIS_JOB_ID;
- } else {
- var currentTime = new Date();
-
- testBuildNumber = "manual-" + currentTime.getTime();
- }
-
- for (var i = 0; i < i18nFiles.length; i++) {
- var file = i18nFiles[i];
- var name = file.split('.')[0];
-
- i18nModules.push({
- name: name
- });
-
- i18nPaths[name] = '../../' + name;
- }
-
- var minifiedBanner = '/*! Select2 <%= package.version %> | https://github.com/select2/select2/blob/master/LICENSE.md */';
-
- grunt.initConfig({
- package: grunt.file.readJSON('package.json'),
-
- concat: {
- 'dist': {
- options: {
- banner: grunt.file.read('src/js/wrapper.start.js'),
- },
- src: [
- 'dist/js/select2.js',
- 'src/js/wrapper.end.js'
- ],
- dest: 'dist/js/select2.js'
- },
- 'dist.full': {
- options: {
- banner: grunt.file.read('src/js/wrapper.start.js'),
- },
- src: [
- 'dist/js/select2.full.js',
- 'src/js/wrapper.end.js'
- ],
- dest: 'dist/js/select2.full.js'
- }
- },
-
- connect: {
- tests: {
- options: {
- base: '.',
- hostname: '127.0.0.1',
- port: 9999
- }
- }
- },
-
- uglify: {
- 'dist': {
- src: 'dist/js/select2.js',
- dest: 'dist/js/select2.min.js',
- options: {
- banner: minifiedBanner
- }
- },
- 'dist.full': {
- src: 'dist/js/select2.full.js',
- dest: 'dist/js/select2.full.min.js',
- options: {
- banner: minifiedBanner
- }
- }
- },
-
- qunit: {
- all: {
- options: {
- urls: testUrls
- }
- }
- },
-
- jshint: {
- options: {
- jshintrc: true,
- reporterOutput: ''
- },
- code: {
- src: ['src/js/**/*.js']
- },
- tests: {
- src: ['tests/**/*.js']
- }
- },
-
- sass: {
- dist: {
- options: {
- outputStyle: 'compressed'
- },
- files: {
- 'dist/css/select2.min.css': [
- 'src/scss/core.scss',
- 'src/scss/theme/default/layout.css'
- ]
- }
- },
- dev: {
- options: {
- outputStyle: 'nested'
- },
- files: {
- 'dist/css/select2.css': [
- 'src/scss/core.scss',
- 'src/scss/theme/default/layout.css'
- ]
- }
- }
- },
-
- requirejs: {
- 'dist': {
- options: {
- baseUrl: 'src/js',
- optimize: 'none',
- name: 'select2/core',
- out: 'dist/js/select2.js',
- include: includes,
- namespace: 'S2',
- paths: {
- 'almond': require.resolve('almond').slice(0, -3),
- 'jquery': 'jquery.shim',
- 'jquery-mousewheel': 'jquery.mousewheel.shim'
- },
- wrap: {
- startFile: 'src/js/banner.start.js',
- endFile: 'src/js/banner.end.js'
- }
- }
- },
- 'dist.full': {
- options: {
- baseUrl: 'src/js',
- optimize: 'none',
- name: 'select2/core',
- out: 'dist/js/select2.full.js',
- include: fullIncludes,
- namespace: 'S2',
- paths: {
- 'almond': require.resolve('almond').slice(0, -3),
- 'jquery': 'jquery.shim',
- 'jquery-mousewheel': require.resolve('jquery-mousewheel').slice(0, -3)
- },
- wrap: {
- startFile: 'src/js/banner.start.js',
- endFile: 'src/js/banner.end.js'
- }
- }
- },
- 'i18n': {
- options: {
- baseUrl: 'src/js/select2/i18n',
- dir: 'dist/js/i18n',
- paths: i18nPaths,
- modules: i18nModules,
- namespace: 'S2',
- wrap: {
- start: minifiedBanner + grunt.file.read('src/js/banner.start.js'),
- end: grunt.file.read('src/js/banner.end.js')
- }
- }
- }
- },
-
- watch: {
- js: {
- files: [
- 'src/js/select2/**/*.js',
- 'tests/**/*.js'
- ],
- tasks: [
- 'compile',
- 'test',
- 'minify'
- ]
- },
- css: {
- files: [
- 'src/scss/**/*.scss'
- ],
- tasks: [
- 'compile',
- 'minify'
- ]
- }
- }
- });
-
- grunt.loadNpmTasks('grunt-contrib-concat');
- grunt.loadNpmTasks('grunt-contrib-connect');
- grunt.loadNpmTasks('grunt-contrib-jshint');
- grunt.loadNpmTasks('grunt-contrib-qunit');
- grunt.loadNpmTasks('grunt-contrib-requirejs');
- grunt.loadNpmTasks('grunt-contrib-uglify');
- grunt.loadNpmTasks('grunt-contrib-watch');
-
- grunt.loadNpmTasks('grunt-sass');
-
- grunt.registerTask('default', ['compile', 'test', 'minify']);
-
- grunt.registerTask('compile', [
- 'requirejs:dist', 'requirejs:dist.full', 'requirejs:i18n',
- 'concat:dist', 'concat:dist.full',
- 'sass:dev'
- ]);
- grunt.registerTask('minify', ['uglify', 'sass:dist']);
- grunt.registerTask('test', ['connect:tests', 'qunit', 'jshint']);
-
- grunt.registerTask('ci', ['compile', 'test']);
-};
diff --git a/LICENSE.md b/LICENSE.md
index 8cb8a2b1..99ec8d0c 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,21 +1,14 @@
-The MIT License (MIT)
+Copyright 2018 Igor Vaynberg and Select2 contributors
-Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the following conditions:
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+Software.
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
index 58e0b70c..5f479eab 100644
--- a/README.md
+++ b/README.md
@@ -1,134 +1,46 @@
-Select2
-=======
-[![Build Status][travis-ci-image]][travis-ci-status]
-[![cdnjs](https://img.shields.io/cdnjs/v/select2.svg)](https://cdnjs.com/libraries/select2)
-[![jsdelivr](https://data.jsdelivr.com/v1/package/npm/select2/badge)](https://www.jsdelivr.com/package/npm/select2)
+# SELECT 2
-Select2 is a jQuery-based replacement for select boxes. It supports searching,
-remote data sets, and pagination of results.
+This branch represents work-in-progress for the next (5.x) version of Select2.
-To get started, checkout examples and documentation at
-https://select2.org/
+Major features of this branch:
-Use cases
----------
-* Enhancing native selects with search.
-* Enhancing native selects with a better multi-select interface.
-* Loading data from JavaScript: easily load items via AJAX and have them
- searchable.
-* Nesting optgroups: native selects only support one level of nesting. Select2
- does not have this restriction.
-* Tagging: ability to add new items on the fly.
-* Working with large, remote datasets: ability to partially load a dataset based
- on the search term.
-* Paging of large datasets: easy support for loading more pages when the results
- are scrolled to the end.
-* Templating: support for custom rendering of results and selections.
+## ADA support
-Browser compatibility
----------------------
-* IE 8+
-* Chrome 8+
-* Firefox 10+
-* Safari 3+
-* Opera 10.6+
+ADA support has been lacking in previous versions of Select2 which made it difficult in incorporate into applications
+where ADA is a requirement. In order to support ADA Select2 had to be redesigned from scratch.
-Select2 is automatically tested on the following browsers.
+## Written in Preact
-[![Sauce Labs Test Status][saucelabs-matrix]][saucelabs-status]
+This version of Select2 is written in Preact. The reason behind this decision is that majority of bugs came from
+state updates being inconsistently applied to the DOM, by using Preact we get DOM updates for free.
-Usage
------
-You can source Select2 directly from a CDN like [JSDliver][jsdelivr] or
-[CDNJS][cdnjs], [download it from this GitHub repo][releases], or use one of
-the integrations below.
+## Native Bridge / Usage Outside Preact
-Integrations
-------------
-Third party developers have created plugins for platforms which allow Select2 to be integrated more natively and quickly. For many platforms, additional plugins are not required because Select2 acts as a standard `` box.
+The fact that the core component is written in Preact does not preclude the usage of Select2 in other environments. To
+this end `select25.js` is provided and allows usage of the widget via native JavaScript.
-Plugins
+## TODO
-* [Django]
- - [django-autocomplete-light]
- - [django-easy-select2]
- - [django-select2]
-* [Drupal] - [drupal-select2]
-* [Meteor] - [meteor-select2]
-* [Ruby on Rails][ruby-on-rails] - [select2-rails]
-* [Wicket] - [wicketstuff-select2]
-* [Yii 2][yii2] - [yii2-widget-select2]
-* [Angularjs][angularjs] - [mdr-angular-select2]
+- So far this branch contains prototype implementations of the Multi-Select and Single-Select widgets. This branch will act as a proof of
+ concept. Once ADA compliance has been validated by the community the rest of the features will follow.
-Themes
+- The visual design / initial theme is still incomplete
-- [Bootstrap 3][bootstrap3] - [select2-bootstrap-theme]
-- [Bootstrap 4][bootstrap4] - [select2-bootstrap4-theme]
-- [Flat UI][flat-ui] - [select2-flat-theme]
-- [Metro UI][metro-ui] - [select2-metro]
+- Mobile design and testing
-Missing an integration? Modify this `README` and make a pull request back here to Select2 on GitHub.
+## Building
-Internationalization (i18n)
----------------------------
-Select2 supports multiple languages by simply including the right language JS
-file (`dist/js/i18n/it.js`, `dist/js/i18n/nl.js`, etc.) after
-`dist/js/select2.js`.
+`npm run dist`
-Missing a language? Just copy `src/js/select2/i18n/en.js`, translate it, and
-make a pull request back to Select2 here on GitHub.
+## Developing
-Documentation
--------------
-The documentation for Select2 is available
-[through GitHub Pages][documentation] and is located within this repository
-in the [`docs` folder][documentation-folder].
+`npm run dev` and open `http://localhost:1234`.
+Sources for dev playground are in `./dev/`
-Community
----------
-You can find out about the different ways to get in touch with the Select2
-community at the [Select2 community page][community].
+## Reporting Bugs
-Copyright and license
----------------------
-The license is available within the repository in the [LICENSE][license] file.
+Please tag GitHub issues and other threads using the `5.x` label
-[cdnjs]: http://www.cdnjs.com/libraries/select2
-[community]: https://select2.org/getting-help
-[documentation]: https://select2.org
-[documentation-folder]: https://github.com/select2/select2/tree/master/docs
-[freenode]: https://freenode.net/
-[jsdelivr]: http://www.jsdelivr.com/#!select2
-[license]: LICENSE.md
-[releases]: https://github.com/select2/select2/releases
-[saucelabs-matrix]: https://saucelabs.com/browser-matrix/select2.svg
-[saucelabs-status]: https://saucelabs.com/u/select2
-[travis-ci-image]: https://img.shields.io/travis/select2/select2/master.svg
-[travis-ci-status]: https://travis-ci.org/select2/select2
+## Copyright and License
-[bootstrap3]: https://getbootstrap.com/
-[bootstrap4]: https://getbootstrap.com/
-[django]: https://www.djangoproject.com/
-[django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light
-[django-easy-select2]: https://github.com/asyncee/django-easy-select2
-[django-select2]: https://github.com/applegrew/django-select2
-[drupal]: https://www.drupal.org/
-[drupal-select2]: https://www.drupal.org/project/select2
-[flat-ui]: http://designmodo.github.io/Flat-UI/
-[meteor]: https://www.meteor.com/
-[meteor-select2]: https://github.com/nate-strauser/meteor-select2
-[metro-ui]: http://metroui.org.ua/
-[select2-metro]: http://metroui.org.ua/select2.html
-[ruby-on-rails]: http://rubyonrails.org/
-[select2-bootstrap-theme]: https://github.com/select2/select2-bootstrap-theme
-[select2-bootstrap4-theme]: https://github.com/ttskch/select2-bootstrap4-theme
-[select2-flat-theme]: https://github.com/techhysahil/select2-Flat_Theme
-[select2-rails]: https://github.com/argerim/select2-rails
-[vue.js]: http://vuejs.org/
-[select2-vue]: http://vuejs.org/examples/select2.html
-[wicket]: https://wicket.apache.org/
-[wicketstuff-select2]: https://github.com/wicketstuff/core/tree/master/select2-parent
-[yii2]: http://www.yiiframework.com/
-[yii2-widget-select2]: https://github.com/kartik-v/yii2-widget-select2
-[angularjs]: https://angularjs.org/
-[mdr-angular-select2]: https://github.com/modulr/mdr-angular-select2
+The license is available within the repository in the LICENSE file.
diff --git a/bower.json b/bower.json
deleted file mode 100644
index 681600b5..00000000
--- a/bower.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "name": "select2",
- "description": "Select2 is a jQuery based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results.",
- "main": [
- "dist/js/select2.js",
- "src/scss/core.scss"
- ],
- "license": "MIT",
- "repository": {
- "type": "git",
- "url": "git@github.com:select2/select2.git"
- }
-}
diff --git a/bridge/src/ajax.ts b/bridge/src/ajax.ts
new file mode 100644
index 00000000..8797fb98
--- /dev/null
+++ b/bridge/src/ajax.ts
@@ -0,0 +1,50 @@
+import { QueryFunction, QueryResult } from '../../control/src/search-controller';
+import { extend } from '../../control/src/util';
+export interface Ajax {
+ url: string;
+ params: (term: string, page: number) => object;
+ process: (data: string) => QueryResult;
+}
+
+export function createQueryFromAjax(ajax: Ajax): QueryFunction {
+ ajax = extend({}, ajax, {
+ params(term: string, page: number) {
+ return { term, page };
+ },
+ process(data: string) {
+ const json = JSON.parse(data);
+ return {
+ more: json.more,
+ values: json.values
+ };
+ }
+ });
+
+ return (term: string, page: number, token: string) => {
+ return new Promise((resolve, reject) => {
+ let url = ajax.url;
+ const params = ajax.params(term, page);
+ if (params) {
+ let separator = url.indexOf('?') >= 0 ? '&' : '?';
+ Object.entries(params).forEach(([key, value]) => {
+ url += separator;
+ separator = '&';
+ url += encodeURIComponent(key) + '=' + encodeURIComponent(value);
+ });
+ }
+
+ const request = new XMLHttpRequest();
+ request.open('GET', url, true);
+ request.onload = () => {
+ if (request.status >= 200 && request.status < 400) {
+ const data = ajax.process(request.responseText);
+ resolve({ values: data.values, more: data.more, token });
+ } else {
+ reject();
+ }
+ };
+ request.onerror = reject;
+ request.send();
+ });
+ };
+}
diff --git a/bridge/src/select25.tsx b/bridge/src/select25.tsx
new file mode 100644
index 00000000..c36b1125
--- /dev/null
+++ b/bridge/src/select25.tsx
@@ -0,0 +1,277 @@
+/** jsx:pragma h */
+import { Component, h, render } from 'preact';
+import { ItemRenderer, QueryFunction } from '../../control/src/abstract-select';
+import { Dictionary } from '../../control/src/dictionary';
+import { MultiSelect } from '../../control/src/multi-select';
+import '../../control/src/select25.scss';
+import { SingleSelect } from '../../control/src/single-select';
+import { extend } from '../../control/src/util';
+import { Ajax, createQueryFromAjax } from './ajax';
+import { Store } from './store';
+
+const forceImportOfH = h;
+
+enum StoreKeys {
+ targetElement = 'te'
+}
+
+export interface Options {
+ multiple: boolean;
+ containerStyle?: string;
+ containerClass?: string;
+ hiddenValue?: (values: any, options: Options) => string;
+ tabIndex?: number;
+ itemId: ((item: any) => string) | string;
+ itemLabel: ((item: any) => string) | string;
+ valueContent?: ItemRenderer;
+ resultContent?: ItemRenderer;
+ query?: QueryFunction;
+ ajax?: Ajax;
+ quiet?: number;
+ minimumCharacters?: number;
+ openOnFocus?: boolean;
+ dictionary?: string | Dictionary;
+
+ value: any;
+ values: any[];
+ allowClear?: boolean;
+ placeholder?: string;
+
+ /** Single Select Label */
+ label?: string;
+
+ /** Multi Select Selected Values Listbox Label */
+ valuesLabel?: string;
+ /** Multi Select Add Value Combobox Label */
+ comboboxLabel?: string;
+
+ allowDuplicates: boolean;
+}
+
+const DEFAULT_OPTIONS = {
+ allowClear: false,
+ dictionary: 'en_us',
+ hiddenValue: (values: any, options: Options) => {
+ const id = (item: any) => {
+ if (typeof options.itemId === 'function') {
+ return options.itemId(item);
+ } else {
+ return '' + item[options.itemId];
+ }
+ };
+
+ if (values) {
+ if (Array.isArray(values)) {
+ if (values.length > 0) {
+ return values.map(id).join(',');
+ } else {
+ return '';
+ }
+ } else {
+ return id(values);
+ }
+ } else {
+ return '';
+ }
+ },
+ minimumCharacters: 0,
+ multiple: false,
+ openOnFocus: false
+};
+
+function triggerOnChange(element: HTMLElement, data: any) {
+ const event = document.createEvent('HTMLEvents');
+ event.initEvent('change', false, true);
+ event[data] = data;
+ element.dispatchEvent(event);
+}
+
+class MultiSelectWrapper extends Component<
+ {
+ element: HTMLInputElement;
+ options: Options;
+ },
+ { values: any }
+> {
+ constructor(props) {
+ super(props);
+ this.state = { values: props.options.values };
+ }
+
+ public componentDidUpdate() {
+ this.setHiddenValue(this.state.values);
+ }
+
+ public componentDidMount() {
+ this.setHiddenValue(this.state.values);
+ }
+
+ public render(props, state, context) {
+ const opts = this.props.options;
+ return (
+
+ );
+ }
+
+ public onChange = (values: any[]) => {
+ this.setState({ values });
+ this.setHiddenValue(values);
+ triggerOnChange(this.props.element, values);
+ };
+
+ private setHiddenValue(values: any) {
+ const { element, options } = this.props;
+ element.value = options.hiddenValue(values, options);
+ }
+}
+
+class SingleSelectWrapper extends Component<
+ {
+ options: Options;
+ element: HTMLInputElement;
+ },
+ { value: any }
+> {
+ constructor(props) {
+ super(props);
+ this.state = { value: props.options.value };
+ }
+
+ public componentDidMount() {
+ this.setHiddenValue(this.state.value);
+ }
+
+ public componentDidUpdate() {
+ this.setHiddenValue(this.state.value);
+ }
+
+ public render(props, state, context) {
+ const opts = this.props.options;
+ return (
+
+ );
+ }
+
+ public onChange = (value: any) => {
+ this.setState({ value });
+ this.setHiddenValue(value);
+ triggerOnChange(this.props.element, value);
+ };
+
+ private setHiddenValue(value: any) {
+ const { element, options } = this.props;
+ element.value = options.hiddenValue(value, options);
+ }
+}
+
+function create(element: HTMLInputElement, options: Options) {
+ // TODO make sure we are attached to hidden input
+
+ const store = Store.getStore(element);
+
+ options = extend({}, DEFAULT_OPTIONS, options);
+ if (!options.query && options.ajax) {
+ options.query = createQueryFromAjax(options.ajax);
+ }
+
+ if (!options.tabIndex && element.tabIndex) {
+ options.tabIndex = element.tabIndex;
+ }
+
+ if (element.getAttribute('s25-style')) {
+ let style = options.containerStyle || '';
+ if (style.length > 0) {
+ style += ';';
+ }
+ style += element.getAttribute('s25-style');
+ options.containerStyle = style;
+ }
+
+ if (element.getAttribute('s25-class')) {
+ let clazz = options.containerClass || '';
+ if (clazz.length > 0) {
+ clazz += ' ';
+ }
+ clazz += element.getAttribute('s25-class');
+ options.containerClass = clazz;
+ }
+
+ // create placeholder element into which the control will be rendered
+ const parentElement = element.parentElement;
+ const targetElement = document.createElement('div');
+ parentElement.insertBefore(targetElement, element);
+
+ store.set(StoreKeys.targetElement, targetElement);
+
+ // render the replacement
+ if (options.multiple) {
+ render( , parentElement, targetElement);
+ } else {
+ render( , parentElement, targetElement);
+ }
+}
+
+function destroy(element: HTMLElement) {
+ if (!Store.hasStore(element)) {
+ return;
+ }
+ const store = Store.getStore(element);
+ const targetElement = store.get(StoreKeys.targetElement);
+ const parentElement = element.parentElement;
+ render(null, parentElement, targetElement);
+ parentElement.removeChild(targetElement);
+ Store.removeStore(element);
+}
+
+const select25 = {
+ create,
+ destroy
+};
+
+export { select25 };
+
+declare global {
+ interface Window {
+ select25: typeof select25;
+ }
+}
+
+window.select25 = select25;
diff --git a/bridge/src/store.ts b/bridge/src/store.ts
new file mode 100644
index 00000000..e276abca
--- /dev/null
+++ b/bridge/src/store.ts
@@ -0,0 +1,29 @@
+export class Store {
+ private static stores = new WeakMap();
+
+ private store = new Map();
+
+ public get(key: string): any {
+ return this.store.get(key);
+ }
+ public set(key: string, value: any) {
+ this.store.set(key, value);
+ }
+
+ public static getStore(key: any): Store {
+ let store = Store.stores.get(key);
+ if (!store) {
+ store = new Store();
+ Store.stores.set(key, store);
+ }
+ return store;
+ }
+
+ public static hasStore(key: any) {
+ return Store.stores.has(key);
+ }
+
+ public static removeStore(key: any) {
+ Store.stores.delete(key);
+ }
+}
diff --git a/component.json b/component.json
deleted file mode 100644
index 75e19f1e..00000000
--- a/component.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "name": "select2",
- "repo": "select/select2",
- "description": "Select2 is a jQuery based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results.",
- "version": "4.0.7",
- "demo": "https://select2.org/",
- "keywords": [
- "jquery"
- ],
- "main": "dist/js/select2.js",
- "styles": [
- "dist/css/select2.css"
- ],
- "scripts": [
- "dist/js/select2.js",
- "dist/js/i18n/*.js"
- ],
- "license": "MIT"
-}
diff --git a/composer.json b/composer.json
deleted file mode 100644
index 5ef2db2b..00000000
--- a/composer.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "name": "select2/select2",
- "description": "Select2 is a jQuery based replacement for select boxes.",
- "type": "component",
- "homepage": "https://select2.org/",
- "license": "MIT",
- "extra": {
- "component": {
- "scripts": [
- "dist/js/select2.js"
- ],
- "styles": [
- "dist/css/select2.css"
- ],
- "files": [
- "dist/js/select2.js",
- "dist/js/i18n/*.js",
- "dist/css/select2.css"
- ]
- }
- }
-}
diff --git a/control/src/abstract-select.tsx b/control/src/abstract-select.tsx
new file mode 100644
index 00000000..cc2ef1ae
--- /dev/null
+++ b/control/src/abstract-select.tsx
@@ -0,0 +1,361 @@
+import { Component, ComponentChild, h, h as createElement } from 'preact';
+import * as announce from './announce';
+import { Dictionary, getDictionary } from './dictionary';
+import { DeepPartial, Key, merge, uuid } from './util';
+const forceImportOfH = h;
+
+type ToString = (item: any) => string;
+
+export type ItemRenderer = (string) | ((item: any, h: typeof createElement) => ComponentChild);
+
+export type QueryFunction = (search: string, page: number, token: string) => Promise;
+
+export interface QueryResult {
+ values: any[];
+ more: boolean;
+ token: string;
+}
+
+export interface ResultListState {
+ results: any[];
+ token: string;
+ active: number;
+ page: number;
+
+ showMinimumCharactersError: boolean;
+ showNoSearchResultsFound: boolean;
+ showLoadMoreResults: boolean;
+}
+export interface State {
+ search: string;
+ results: ResultListState;
+ loading: boolean;
+ open: boolean;
+ focused: boolean;
+}
+
+export interface Props {
+ containerStyle?: string;
+ containerClass?: string;
+ tabIndex?: number;
+ itemId: ToString | string;
+ itemLabel: ToString | string;
+ valueContent?: ItemRenderer;
+ resultContent?: ItemRenderer;
+
+ query: QueryFunction;
+ quiet?: number;
+ allowDuplicates?: boolean;
+ minimumCharacters?: number;
+ openOnFocus?: boolean;
+ dictionary?: string | Dictionary;
+}
+
+function MarkupRenderer({ markup }) {
+ return
;
+}
+
+export const DEFAULT_PROPS: Partial = {
+ allowDuplicates: false,
+ minimumCharacters: 0,
+ quiet: 50,
+ tabIndex: 0
+};
+
+export abstract class AbstractSelect extends Component
{
+ private searchTimeout: number | undefined;
+ protected namespace: string;
+
+ constructor(props: P) {
+ super(props);
+ this.searchTimeout = undefined;
+ this.namespace = uuid();
+ // @ts-ignore
+ this.state = {
+ focused: false,
+ loading: false,
+ open: false,
+ results: {
+ active: -1,
+ page: 0,
+ results: undefined, // TODO rename to values
+ token: null,
+
+ showLoadMoreResults: false,
+ showMinimumCharactersError: false,
+ showNoSearchResultsFound: false
+ },
+ search: ''
+ };
+ }
+
+ public getItemId = (item: any): string => {
+ const id = this.props.itemId;
+ if (typeof id === 'function') {
+ return (id as ToString)(item);
+ } else {
+ return '' + item[id];
+ }
+ };
+
+ public getItemLabel = (item: any): string => {
+ const label = this.props.itemLabel;
+ if (typeof label === 'function') {
+ return (label as ToString)(item);
+ } else {
+ return '' + item[label];
+ }
+ };
+
+ public renderValue = (item: any): ComponentChild => {
+ return this.renderItem(item, 'valueContent');
+ };
+
+ public renderResult = (item: any): ComponentChild => {
+ return this.renderItem(item, 'resultContent');
+ };
+
+ private renderItem = (item: any, rendererName: keyof Props): ComponentChild => {
+ const renderer = this.props[rendererName] as ItemRenderer;
+ if (renderer) {
+ if (typeof renderer === 'function') {
+ const render = renderer(item, createElement);
+ if (typeof render === 'string') {
+ return ;
+ } else {
+ return render;
+ }
+ } else {
+ return ;
+ }
+ } else {
+ return ;
+ }
+ };
+
+ get dictionary(): Dictionary {
+ const dict = this.props.dictionary;
+ if (dict) {
+ if (typeof dict === 'string') {
+ return getDictionary(dict);
+ } else {
+ return dict as Dictionary;
+ }
+ } else {
+ return getDictionary();
+ }
+ }
+
+ protected updateState(update: DeepPartial | Array>, callback?: () => void) {
+ const state = merge(this.state, Array.isArray(update) ? update : [update]);
+ this.setState(state, callback);
+ }
+
+ public search = (query, selectedValues, start?: DeepPartial, callback?: () => void) => {
+ const dictionary = this.dictionary;
+ const { minimumCharacters, allowDuplicates, quiet, query: queryFunc } = this.props;
+
+ const current = this.state.results;
+
+ const minimumCharactersReached = query.length >= minimumCharacters;
+ const token = minimumCharactersReached ? uuid() : undefined;
+
+ const control = this;
+
+ this.updateState(
+ // @ts-ignore
+ [
+ start,
+ {
+ loading: minimumCharactersReached,
+ results: {
+ active: -1,
+ page: 0,
+ results: undefined,
+ showLoadMoreResults: false,
+ showMinimumCharactersError: !minimumCharactersReached,
+ showNoSearchResultsFound: false,
+ token
+ },
+ search: query
+ }
+ ],
+ () => {
+ if (callback) {
+ callback();
+ }
+
+ if (!minimumCharactersReached) {
+ // todo - throttle this announcement?
+ announce.politely(dictionary.minimumCharactersMessage(query.length, minimumCharacters!));
+ return;
+ }
+
+ // todo - throttle this announcement?
+ // announce.politely(dictionary.searchResultsLoading());
+
+ const execute = async () => {
+ try {
+ const result = await queryFunc(query, 0, token!);
+ if (result.token !== control.state.results.token) {
+ // this is a stale result, ignore
+ return;
+ }
+
+ let values = result.values || [];
+ if (!allowDuplicates && values.length > 0 && selectedValues.length > 0) {
+ const ids = new Set();
+ selectedValues.forEach(v => ids.add(control.getItemId(v)));
+ values = values.filter(v => !ids.has(control.getItemId(v)));
+ }
+
+ if (values.length < 1) {
+ announce.politely(dictionary.noSearchResults());
+ }
+
+ // @ts-ignore
+ control.updateState({
+ loading: false,
+ results: {
+ active: values.length > 0 ? 0 : -1,
+ page: 0,
+ results: values,
+ showLoadMoreResults: result.more,
+ showNoSearchResultsFound: values.length < 1
+ }
+ });
+ } catch (e) {
+ // @ts-ignore
+ control.updateState({ loading: false });
+ }
+ };
+
+ if (quiet && quiet > 0) {
+ if (control.searchTimeout) {
+ window.clearTimeout(control.searchTimeout);
+ }
+ control.searchTimeout = window.setTimeout(execute, quiet);
+ } else {
+ execute();
+ }
+ }
+ );
+ };
+
+ public loadMore() {
+ const {
+ loading,
+ search: query,
+ results: { page }
+ } = this.state;
+ const dict = this.dictionary;
+ const { query: queryFunc } = this.props;
+ const control = this;
+
+ if (loading) {
+ return;
+ }
+
+ const token = uuid();
+ const nextPage = page + 1;
+
+ this.updateState(
+ // @ts-ignore
+ {
+ loading: true,
+ results: {
+ token
+ }
+ },
+ async () => {
+ // TODO throttle?
+ // announce.politely(dict.searchResultsLoading());
+
+ try {
+ const result = await queryFunc(query, nextPage, token);
+
+ const current = control.state.results;
+
+ if (result.token !== current.token) {
+ // this is a stale result, ignore
+ return;
+ }
+
+ if (result.values && result.values.length > 0) {
+ // @ts-ignore
+ control.updateState({
+ loading: false,
+ results: {
+ page: nextPage,
+ results: current.results.concat(result.values),
+ showLoadMoreResults: result.more
+ }
+ });
+ } else {
+ announce.politely(dict.noSearchResults());
+ // @ts-ignore
+ control.updateState({
+ loading: false,
+ results: {
+ showLoadMoreResults: false
+ }
+ });
+ }
+ } catch (e) {
+ // @ts-ignore
+ control.updateState({ loading: false });
+ }
+ }
+ );
+ }
+
+ protected handleResultNavigationKeyDown(event: KeyboardEvent): boolean {
+ switch (event.key) {
+ case Key.ArrowUp:
+ case Key.Up:
+ this.selectPreviousSearchResult();
+ event.preventDefault();
+ return true;
+ case Key.ArrowDown:
+ case Key.Down:
+ this.selectNextSearchResult();
+ event.preventDefault();
+ return true;
+ }
+ return false;
+ }
+
+ protected selectNextSearchResult() {
+ const { active, results } = this.state.results;
+ if (results && active < results.length - 1) {
+ // @ts-ignore
+ this.updateState({ results: { active: active + 1 } });
+ }
+ }
+
+ protected selectPreviousSearchResult() {
+ const { active } = this.state.results;
+ if (active > 0) {
+ // @ts-ignore
+ this.updateState({ results: { active: active - 1 } });
+ }
+ }
+
+ protected getSelectedSearchResult() {
+ const { results, active } = this.state.results;
+ return results[active];
+ }
+
+ protected selectSearchResult(index: number) {
+ const { active } = this.state.results;
+ if (active !== index) {
+ // @ts-ignore
+ this.updateState({ results: { active: index } });
+ }
+ }
+
+ protected hasSearchResults() {
+ const results = this.state.results.results;
+ return results && results.length > 0;
+ }
+}
diff --git a/control/src/announce.ts b/control/src/announce.ts
new file mode 100644
index 00000000..72d5d9fd
--- /dev/null
+++ b/control/src/announce.ts
@@ -0,0 +1,49 @@
+export function initialize() {
+ if (document.getElementById('s25-live')) {
+ return;
+ }
+
+ const live = document.createElement('div');
+ live.setAttribute('id', 's25-live');
+ live.setAttribute('class', 's25-offscreen s25-live');
+ document.body.appendChild(live);
+
+ const assertive = document.createElement('div');
+ assertive.setAttribute('id', 's25-live-assertive');
+ assertive.setAttribute('role', 'log');
+ assertive.setAttribute('aria-live', 'assertive');
+ assertive.setAttribute('aria-relevant', 'additions');
+ live.appendChild(assertive);
+
+ const polite = document.createElement('div');
+ polite.setAttribute('id', 's25-live-polite');
+ polite.setAttribute('role', 'log');
+ polite.setAttribute('aria-live', 'polite');
+ polite.setAttribute('aria-relevant', 'additions');
+ live.appendChild(polite);
+}
+
+export function assertively(message: string) {
+ add(message, document.getElementById('s25-live-assertive')!);
+}
+
+export function politely(message: string) {
+ add(message, document.getElementById('s25-live-polite')!);
+}
+
+function add(message: string, container: HTMLElement) {
+ const node = document.createElement('div');
+ node.appendChild(document.createTextNode(message));
+ container.appendChild(node);
+
+ // clean up old nodes
+
+ let collection = document.getElementById('s25-live-assertive')!;
+ while (collection.firstChild && collection.firstChild !== node) {
+ collection.removeChild(collection.firstChild);
+ }
+ collection = document.getElementById('s25-live-polite')!;
+ while (collection.firstChild && collection.firstChild !== node) {
+ collection.removeChild(collection.firstChild);
+ }
+}
diff --git a/control/src/dictionary.ts b/control/src/dictionary.ts
new file mode 100644
index 00000000..44ad7154
--- /dev/null
+++ b/control/src/dictionary.ts
@@ -0,0 +1,58 @@
+export interface Dictionary {
+ valueAdded(itemLabel: string): string;
+ noSearchResults(): string;
+ searchResultsLoading(): string;
+ removeButtonTitle(): string;
+ clearButtonTitle(): string;
+ minimumCharactersMessage(len: number, min: number): string;
+ multiSelectInstructions(): string;
+}
+
+const EN_US: Dictionary = {
+ noSearchResults() {
+ return 'No results available';
+ },
+
+ searchResultsLoading() {
+ return 'Loading...';
+ },
+
+ removeButtonTitle() {
+ return 'Remove selected values';
+ },
+
+ clearButtonTitle() {
+ return 'Clear selection';
+ },
+
+ valueAdded(itemLabel: string) {
+ return itemLabel + ' added';
+ },
+
+ minimumCharactersMessage(len: number, min: number) {
+ const delta = min - len;
+ return 'Please enter ' + delta + ' more character' + (delta > 1 ? 's' : '');
+ },
+
+ multiSelectInstructions(): string {
+ return "Items can be removed from this list box by selecting them and activating 'Remove selected values' button. Items can be added by selecting them in the adjacent combobox.";
+ }
+};
+
+const dictionaries = new Map();
+dictionaries.set('en_us', EN_US);
+
+export function getDictionary(dict?: string | undefined): Dictionary {
+ const fallback: Dictionary = dictionaries.get('en_us') as Dictionary;
+
+ if (!dict) {
+ return fallback;
+ }
+
+ if (typeof dict === 'string') {
+ const instance = dictionaries.get(dict);
+ return instance ? instance : fallback;
+ } else {
+ return dict;
+ }
+}
diff --git a/control/src/dropdown.tsx b/control/src/dropdown.tsx
new file mode 100644
index 00000000..b61eedbe
--- /dev/null
+++ b/control/src/dropdown.tsx
@@ -0,0 +1,108 @@
+import { Component, ComponentChild, Fragment, h, RefObject, render } from 'preact';
+import { getScrollParents, MouseEventListener, throttle } from './util';
+
+class ContextProvider extends Component<{ context: any }> {
+ public getChildContext() {
+ return this.props.context;
+ }
+ public render() {
+ return this.props.children;
+ }
+}
+
+function Portal({ vnode, container }): null {
+ // @ts-ignore
+ const wrap = {vnode} ;
+ render(wrap, container);
+ return null;
+}
+
+function createPortal(vnode, container): ComponentChild {
+ return h(Portal, { vnode, container });
+}
+
+interface Props {
+ controlRef: RefObject;
+ dropdownRef: RefObject;
+ class?: string;
+ onClick?: MouseEventListener;
+ onMouseDown?: MouseEventListener;
+ onFocusOut?: EventListener;
+}
+
+export class Dropdown extends Component {
+ private container?: HTMLElement;
+ private scrollParents?: EventTarget[];
+ private throttledPosition;
+
+ constructor(props) {
+ super(props);
+ this.throttledPosition = throttle(50, this.position.bind(this));
+ }
+
+ public componentWillMount() {
+ this.container = document.createElement('div');
+ if (this.props.class) {
+ this.container.className = this.props.class;
+ }
+
+ /*
+ this container needs to be able to receive focus so we can tell
+ it is not leaving the control - we consider dropdown part of the control
+ */
+ this.container.tabIndex = -1;
+ if (this.props.onClick) {
+ this.container.addEventListener('click', this.props.onClick);
+ }
+ if (this.props.onMouseDown) {
+ this.container.addEventListener('mousedown', this.props.onMouseDown);
+ }
+ if (this.props.onFocusOut) {
+ this.container.addEventListener('focusout', this.props.onFocusOut);
+ }
+ document.body.appendChild(this.container);
+ }
+
+ public componentDidMount() {
+ this.props.dropdownRef.current = this.container;
+ this.scrollParents = getScrollParents(this.props.controlRef.current!);
+ this.scrollParents.forEach(parent => {
+ ['resize', 'scroll', 'touchmove'].forEach(event => {
+ parent.addEventListener(event, this.throttledPosition);
+ });
+ });
+ this.position();
+ }
+
+ public componentWillUnmount() {
+ if (this.scrollParents) {
+ this.scrollParents.forEach(parent => {
+ ['resize', 'scroll', 'touchmove'].forEach(event => {
+ parent.removeEventListener(event, this.throttledPosition);
+ });
+ });
+ delete this.scrollParents;
+ this.scrollParents = undefined;
+ }
+
+ this.props.dropdownRef.current = undefined;
+ this.container!.parentElement!.removeChild(this.container!);
+ }
+
+ public componentDidUpdate() {
+ this.position();
+ }
+
+ public render(props) {
+ return createPortal({this.props.children} , this.container!);
+ }
+
+ private position() {
+ const control = this.props.controlRef.current!;
+ const rect = control.getBoundingClientRect();
+ const style = `top: ${rect.top + rect.height + window.pageYOffset}px;
+ left: ${rect.left + window.pageXOffset}px;
+ width: ${rect.width}px;`;
+ this.container!.setAttribute('style', style);
+ }
+}
diff --git a/control/src/icons.tsx b/control/src/icons.tsx
new file mode 100644
index 00000000..75a8a5ee
--- /dev/null
+++ b/control/src/icons.tsx
@@ -0,0 +1,28 @@
+import { FunctionComponent, h } from 'preact';
+
+const forceImportOfH = h;
+
+interface Props {
+ width: number;
+ height: number;
+}
+
+export const Toggle: FunctionComponent = ({ height, width }) => {
+ const viewBox = '0 0 ' + width + ' ' + height;
+ return (
+
+
+
+ );
+};
+Toggle.displayName = 'Toggle';
+
+export const Remove: FunctionComponent = ({ width, height }) => {
+ const viewBox = '0 0 ' + width + ' ' + height;
+ return (
+
+
+
+ );
+};
+Remove.displayName = 'Remove';
diff --git a/control/src/multi-select.tsx b/control/src/multi-select.tsx
new file mode 100644
index 00000000..6009525a
--- /dev/null
+++ b/control/src/multi-select.tsx
@@ -0,0 +1,475 @@
+import { createRef, Fragment, h, RefObject } from 'preact';
+import {
+ AbstractSelect,
+ DEFAULT_PROPS as ABSTRACT_DEFAULT_PROPS,
+ Props as SearchControllerProps,
+ State as AbstractSelectState
+} from './abstract-select';
+import * as announce from './announce';
+import { Dropdown } from './dropdown';
+import { Remove, Toggle } from './icons';
+import { ResultList } from './result-list';
+import { style } from './style';
+import { cn, extend, Key, scope } from './util';
+
+const forceImportOfH = h;
+
+export interface Props extends SearchControllerProps {
+ valuesLabel: string;
+ comboboxLabel: string;
+ values: any[];
+ onChange: (values: any[]) => void;
+}
+
+interface ValueListState {
+ active: number;
+ selected: boolean[];
+}
+
+interface State extends AbstractSelectState {
+ values: ValueListState;
+}
+
+const DEFAULT_PROPS = extend({}, ABSTRACT_DEFAULT_PROPS, {});
+
+export class MultiSelect extends AbstractSelect {
+ private containerRef: RefObject;
+ private dropdownRef: RefObject;
+ private bodyRef: RefObject;
+ private valuesRef: RefObject;
+ private searchRef: RefObject;
+
+ public static defaultProps = DEFAULT_PROPS;
+
+ constructor(props) {
+ super(props);
+ const { values } = props;
+
+ this.valuesRef = createRef();
+ this.searchRef = createRef();
+ this.bodyRef = createRef();
+ this.containerRef = createRef();
+ this.dropdownRef = createRef();
+
+ this.state = extend(this.state, {
+ values: {
+ active: -1,
+ selected: values.map(v => false)
+ }
+ });
+ }
+
+ public componentWillMount() {
+ announce.initialize();
+ }
+
+ public render(props, state) {
+ const { values, tabIndex, minimumCharacters, valuesLabel, comboboxLabel } = props;
+ const {
+ open,
+ loading,
+ focused,
+ search,
+ values: { active, selected },
+ results
+ } = state;
+ const dictionary = this.dictionary;
+
+ let classes = cn(style.control, style.multi, { [style.open]: open }, { [style.focused]: focused });
+ if (props.containerClass && props.containerClass.length > 0) {
+ classes += ' ' + props.containerClass;
+ }
+
+ const instructionsDomId = this.namespace + '-instructions';
+ const resultsDomId = this.namespace + '-results';
+ const resultsNamespace = this.namespace + '-res-';
+ return (
+
+
+
+
+ {dictionary.multiSelectInstructions()}
+
+ {scope(() => {
+ const activeDescendant = active >= 0 ? this.namespace + '-vl-' + active : undefined;
+ if (values && values.length > 0) {
+ return (
+
+ {values.map((value: any, index: number) => {
+ const isSelected = selected[index];
+ const isActive = active === index;
+ const css = cn(style.item, {
+ [style.selected]: isSelected,
+ [style.active]: isActive
+ });
+ const id = this.namespace + '-vl-' + index;
+ const label = this.getItemLabel(value);
+ const render = this.renderValue(value);
+ return (
+
e.stopPropagation()}
+ onClick={this.onValueClick(index)}
+ >
+
{render}
+
+ );
+ })}
+
+ );
+ } else {
+ return null;
+ }
+ })}
+ {scope(() => {
+ const disabled = !selected.find(x => x === true);
+ const className = cn(style.remove, {
+ [style.offscreen]: values.length < 1
+ });
+ return (
+
+
+
+
+
+ );
+ })}
+
+ {props.comboboxLabel}
+
+
= 0 ? resultsNamespace + results.active : undefined}
+ aria-busy={loading}
+ onInput={this.onSearchInput}
+ onKeyDown={this.onSearchKeyDown}
+ onFocus={this.onSearchFocus}
+ />
+
+
+
+
+
+ {open && (
+
+
+
+ )}
+
+ );
+ }
+
+ public componentDidMount() {
+ const css = this.props.containerStyle;
+ if (css && css.length > 0) {
+ this.containerRef.current!.setAttribute('style', css);
+ }
+ }
+
+ private onLoadMoreResults = () => {
+ this.loadMore();
+ };
+
+ private focusSearchAndStopPropagation = (event: Event) => {
+ this.searchRef.current!.focus();
+ event.preventDefault();
+ event.stopPropagation();
+ };
+
+ public onToggleClick = (event: MouseEvent) => {
+ const { open } = this.state;
+ if (open) {
+ this.close();
+ this.searchRef.current!.focus();
+ } else {
+ this.search(this.state.search, this.props.values, { open: true });
+ this.searchRef.current!.focus();
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ };
+
+ public onBodyClick = (event: MouseEvent) => {
+ if (event.target === this.bodyRef.current) {
+ // if the element itself was clicked, (white space inside the body)
+ this.searchRef.current!.focus();
+ }
+ };
+
+ public onFocusIn = (event: FocusEvent) => {
+ this.updateState({ focused: true });
+ };
+
+ public onFocusOut = (event: FocusEvent) => {
+ const receiver = event.relatedTarget as Node;
+ const container = this.containerRef.current!;
+ const dropdown = this.dropdownRef.current!;
+ const focused =
+ container.contains(receiver) || (dropdown && (dropdown === receiver || dropdown.contains(receiver)));
+
+ this.updateState({
+ focused
+ });
+ if (!focused) {
+ // this.closeIfOpen();
+ }
+ };
+
+ public onSearchFocus = (event: FocusEvent) => {
+ const { openOnFocus } = this.props;
+ const { open } = this.state;
+ if (!open && openOnFocus) {
+ this.search(this.searchRef.current!.value, this.props.values, { open: true });
+ }
+ };
+
+ public onResultMouseMove = (index: number, event: MouseEvent) => {
+ this.selectSearchResult(index);
+ };
+
+ public selectActiveResult = () => {
+ this.selectResult(this.getSelectedSearchResult());
+ };
+
+ public selectResult = (result: any) => {
+ const { values, onChange } = this.props;
+ const next = values.slice();
+ next.push(result);
+
+ this.close();
+
+ const label = this.getItemLabel(result);
+ announce.politely(this.dictionary.valueAdded(label));
+
+ onChange(next);
+ };
+
+ public toggleValue = (index: number) => {
+ const {
+ values: { selected }
+ } = this.state;
+ const next = selected.slice();
+ next[index] = !next[index];
+ this.updateState({ values: { selected: next, active: index } });
+ };
+
+ public onRemoveSelectedFocus = (event: FocusEvent) => {
+ this.closeIfOpen();
+ };
+
+ public onRemoveSelectedClick = (event: Event) => {
+ const {
+ values: { selected }
+ } = this.state;
+ const { values, onChange } = this.props;
+ const next = values.slice().filter((value, index) => !selected[index]);
+ this.updateState({
+ values: {
+ selected: next.map(v => false)
+ }
+ });
+ onChange(next);
+
+ this.searchRef.current!.focus();
+ };
+
+ public onSearchInput = (event: Event) => {
+ const value = (event.target as HTMLInputElement).value;
+ this.search(value, this.props.values, { open: true });
+ };
+
+ public onSearchKeyDown = (event: KeyboardEvent) => {
+ if (this.handleResultNavigationKeyDown(event)) {
+ return;
+ }
+
+ const { open } = this.state;
+
+ if (open && this.hasSearchResults) {
+ switch (event.key) {
+ case Key.Enter:
+ this.selectActiveResult();
+ event.preventDefault();
+ break;
+ case Key.Escape:
+ if (open) {
+ this.close();
+ }
+ event.preventDefault();
+ break;
+ }
+ }
+ };
+
+ public onValueClick = (index: number) => (event: MouseEvent) => {
+ this.toggleValue(index);
+ event.preventDefault();
+ event.stopPropagation();
+ };
+
+ public onValuesFocus = (event: Event) => {
+ const {
+ values: { active, selected }
+ } = this.state;
+ const { values } = this.props;
+
+ // highlight the first selected value
+ if (active < 0 && values.length > 0) {
+ let index = 0;
+ for (let i = 0; i < selected.length; i++) {
+ if (selected[i]) {
+ index = i;
+ break;
+ }
+ }
+ this.updateState({ values: { active: index } });
+ }
+ this.closeIfOpen();
+ };
+
+ public closeIfOpen() {
+ if (this.state.open) {
+ this.close();
+ }
+ }
+
+ public close = () => {
+ this.updateState({
+ open: false,
+ results: { results: undefined },
+ search: ''
+ });
+ };
+
+ public onValuesBlur = (event: Event) => {
+ this.updateState({ values: { active: -1 } });
+ };
+
+ public onValuesKeyDown = (event: KeyboardEvent) => {
+ const active = this.state.values.active;
+ const { values } = this.props;
+
+ switch (event.key) {
+ case Key.ArrowLeft:
+ case Key.ArrowUp:
+ case Key.Up:
+ case Key.Left: {
+ if (active > 0) {
+ this.updateState({ values: { active: active - 1 } });
+ }
+ event.preventDefault();
+ break;
+ }
+ case Key.ArrowRight:
+ case Key.Right:
+ case Key.ArrowDown:
+ case Key.Down: {
+ if (active < values.length - 1) {
+ this.updateState({ values: { active: active + 1 } });
+ }
+ event.preventDefault();
+ break;
+ }
+ case Key.PageDown: {
+ // TODO
+ event.preventDefault();
+ break;
+ }
+ case Key.PageUp: {
+ // TODO
+ event.preventDefault();
+ break;
+ }
+ case Key.Home: {
+ this.updateState({ values: { active: 0 } });
+ event.preventDefault();
+ break;
+ }
+ case Key.End: {
+ this.updateState({ values: { active: values.length - 1 } });
+ event.preventDefault();
+ break;
+ }
+ case Key.Space:
+ case Key.Spacebar: {
+ this.toggleValue(active);
+ event.preventDefault();
+ break;
+ }
+ }
+ };
+
+ public onDropdownClick = (event: MouseEvent) => {
+ // result clicks do not make it this far because they do not propagate
+ // so this click is on something other than result
+ event.preventDefault();
+ event.stopPropagation();
+ this.searchRef.current!.focus();
+ };
+
+ public onResultClicked = (result: any, event: MouseEvent) => {
+ this.selectResult(result);
+ this.searchRef.current!.focus();
+ event.preventDefault();
+ event.stopPropagation();
+ };
+}
diff --git a/control/src/result-list.tsx b/control/src/result-list.tsx
new file mode 100644
index 00000000..1e2d0af5
--- /dev/null
+++ b/control/src/result-list.tsx
@@ -0,0 +1,190 @@
+import { Component, ComponentChild, createRef, h, RefObject } from 'preact';
+import { Dictionary } from './dictionary';
+import { style } from './style';
+import { calculateVerticalVisibility, cn } from './util';
+
+const forceImportOfH = h;
+
+// TODO if upon render loading is visible signal loadmore
+
+interface Props {
+ // TODO consistenly call search a query
+ namespace: string;
+ search: string;
+ listboxDomId?: string;
+ results: any[];
+ token: string;
+ active: number;
+ page: number;
+ loading: boolean;
+
+ itemLabel: (item: any) => string;
+ renderItem: (item: any) => ComponentChild;
+
+ dictionary: Dictionary;
+ minimumCharacters: number;
+
+ showMinimumCharactersError: boolean;
+ showNoSearchResultsFound: boolean;
+ showLoadMoreResults: boolean;
+
+ onResultClicked: (result: any, event: MouseEvent) => void;
+ onMouseMove: (index: any, event: MouseEvent) => void;
+ onLoadMore: () => void;
+}
+
+export class ResultList extends Component {
+ private container: RefObject;
+ private lastMouseClientX?: number;
+ private lastMouseClientY?: number;
+ private loadMore: RefObject;
+ constructor(props) {
+ super(props);
+ this.container = createRef();
+ this.loadMore = createRef();
+ }
+
+ private getResultDomId(index: number) {
+ return this.props.namespace + index;
+ }
+
+ public render(props, state, context) {
+ const { dictionary, minimumCharacters, showLoadMoreResults, results } = props;
+ const query = this.props.search;
+
+ return (
+
+
+ {props.loading && (
+
+ {dictionary.searchResultsLoading()}
+
+ )}
+ {props.showNoSearchResultsFound && (
+
+ {dictionary.noSearchResults()}
+
+ )}
+ {props.showMinimumCharactersError && (
+
+ {dictionary.minimumCharactersMessage(query.length, minimumCharacters)}
+
+ )}
+ {results && results.length > 0 && (
+
= 0 ? this.getResultDomId(props.active) : undefined}
+ >
+ {results.map((result, index) => {
+ const label = props.itemLabel(result);
+ const render = props.renderItem(result);
+ const active = props.active === index;
+ const css = cn(style.item, {
+ [style.active]: active
+ });
+ const id = this.getResultDomId(index);
+ return (
+
+ );
+ })}
+
+ )}
+ {props.showLoadMoreResults && (
+
+ {dictionary.searchResultsLoading()}
+
+ )}
+
+
+ );
+ }
+
+ private onResultClicked = (result: any) => (event: MouseEvent) => {
+ this.props.onResultClicked(result, event);
+ };
+
+ private onMouseMove = (index: number) => (event: MouseEvent) => {
+ if (this.lastMouseClientX === event.clientX && this.lastMouseClientY === event.clientY) {
+ // the mouse did not move, the dropdown was scrolled instead, we do not change selected element because
+ // it will be scrolled into view and mess with the scrolling of the results in the dropdown
+ return;
+ }
+ this.lastMouseClientX = event.clientX;
+ this.lastMouseClientY = event.clientY;
+
+ this.props.onMouseMove(index, event);
+ };
+
+ private onScroll = (event: Event) => {
+ if (!this.props.showLoadMoreResults) {
+ return;
+ }
+ const more = this.loadMore.current!;
+ const drop = this.container.current!;
+
+ const visibility = calculateVerticalVisibility(drop, more);
+ if (visibility !== 'hidden') {
+ this.props.onLoadMore();
+ }
+ };
+
+ public componentDidUpdate(prevProps: Props, prevState: Props) {
+ const { active, results, showLoadMoreResults } = this.props;
+ const { active: prevActive } = prevProps;
+
+ if (active !== prevActive) {
+ if (active >= 0 && results && results.length > 0 && active === results.length - 1 && showLoadMoreResults) {
+ // last result is selected and load more is shown, make sure it is scrolled into view
+
+ const drop = this.container.current!;
+ const el = this.loadMore.current!;
+
+ drop.scrollTop = el.offsetTop + el.offsetHeight - drop.clientHeight;
+
+ // console.log("scrolling to see load more");//, setting scrolltop", drop, el, el.offsetTop - drop.clientHeight);
+ } else if (active >= 0) {
+ // make sure it is scrolled into view
+ const id = this.getResultDomId(active);
+ const el = document.getElementById(id);
+ if (el != null) {
+ const drop = this.container!.current!;
+ const c = drop.getBoundingClientRect();
+ const e = el.getBoundingClientRect();
+
+ if (e.top < c.top && e.bottom <= c.bottom) {
+ const delta = c.top - e.top;
+ drop.scrollTop = drop.scrollTop - delta;
+ }
+
+ if (e.top >= c.top && e.bottom > c.bottom) {
+ const delta = e.bottom - c.bottom;
+ drop.scrollTop = drop.scrollTop + delta;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/control/src/select25.scss b/control/src/select25.scss
new file mode 100644
index 00000000..a27dd769
--- /dev/null
+++ b/control/src/select25.scss
@@ -0,0 +1,311 @@
+$focus-color: #4d90fe;
+$item-bkg-color: #0073e6;
+$item-text-color: #fff;
+$remove-bkg-color: lighten($item-bkg-color, 50%);
+$border: 1px solid #ddd;
+$border-focus: 1px solid $focus-color;
+$border-radius: 5px;
+$dropdown-padding: 8px 10px;
+
+.s25-hidden-accessible {
+ border: 0 !important;
+ clip: rect(0 0 0 0) !important;
+ -webkit-clip-path: inset(50%) !important;
+ clip-path: inset(50%) !important;
+ height: 1px !important;
+ overflow: hidden !important;
+ padding: 0 !important;
+ position: absolute !important;
+ width: 1px !important;
+ white-space: nowrap !important;
+}
+
+.s25-hidden {
+ display: none;
+}
+
+.s25-search {
+ font-size: 100%;
+ line-height: 24px;
+
+ &:focus {
+ outline: 0;
+ }
+}
+
+.s25-control {
+ position: relative;
+ box-sizing: border-box;
+ min-height: 36px;
+ width: 100%;
+
+ border: $border;
+ background-color: #fff;
+ border-radius: $border-radius;
+
+ &.s25-focused {
+ border: $border-focus;
+
+ &.s25-open {
+ border-bottom: none;
+ border-radius: $border-radius $border-radius 0 0;
+ }
+ }
+
+ .s25-body {
+ margin: 3px;
+ margin-bottom: 0;
+ }
+
+ .s25-toggle {
+ &:focus {
+ outline: 0px;
+ }
+
+ svg {
+ display: inline-block;
+ margin: auto;
+ }
+ }
+
+ .s25-toggle {
+ display: flex;
+ box-sizing: border-box;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 30px;
+ width: 30px;
+ transition: right 0.2s;
+ }
+}
+
+.s25-control.s25-multi {
+ display: flex;
+ flex-wrap: nowrap;
+
+ .s25-body {
+ flex-grow: 1;
+ display: flex;
+ flex-wrap: wrap;
+ margin-right: 60px;
+ width: calc(100% - 60px);
+ }
+
+ .s25-multi-values {
+ box-sizing: border-box;
+ display: flex;
+ flex-wrap: wrap;
+ padding: 0;
+
+ .s25-item {
+ position: relative;
+ padding: 5px 12px 5px 18px;
+ border-radius: $border-radius;
+ background-color: $item-bkg-color;
+ color: $item-text-color;
+ margin-right: 3px;
+ margin-bottom: 3px;
+ transition: 0.2s;
+
+ &:hover {
+ background-color: darken($item-bkg-color, 5%);
+ }
+
+ &.s25-active {
+ box-shadow: inset 0 0 0 3px #67b3ff;
+ }
+
+ &.s25-item.s25-selected {
+ text-align: right;
+
+ &:before {
+ content: '✓';
+ position: absolute;
+ left: 5px;
+ }
+ }
+ }
+
+ .s25-label {
+ color: rgb(34, 34, 34);
+ font-size: 85%;
+ border-radius: $border-radius;
+ padding: 3px 3px 3px 6px;
+ }
+ }
+
+ .s25-multi-values:focus {
+ outline: 0;
+ }
+
+ .s25-item {
+ cursor: pointer;
+ box-sizing: border-box;
+ align-items: center;
+ }
+
+ .s25-search {
+ max-width: 100%;
+ min-width: 75px;
+ border: none;
+
+ &:focus {
+ outline: 0;
+ }
+ }
+}
+
+.s25-single.s25-control {
+ height: 36px;
+
+ .s25-body {
+ cursor: pointer;
+ flex-grow: 1;
+ display: flex;
+ flex-wrap: wrap;
+ width: calc(100% - 30px);
+ }
+
+ .s25-value {
+ border: 0px;
+ line-height: 27px;
+ padding-left: 3px;
+
+ &:focus {
+ outline: 0px;
+ }
+ }
+}
+
+.s25-single.s25-dropdown {
+ .s25-search {
+ width: calc(100% - 16px);
+ padding: 6px;
+ margin: $dropdown-padding;
+ box-sizing: border-box;
+ border: $border;
+ outline: 0;
+ }
+}
+
+.s25-offscreen {
+ clip-path: inset(100%);
+ clip: rect(1px, 1px, 1px, 1px);
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ position: absolute;
+ white-space: nowrap;
+}
+
+.s25-dropdown {
+ position: absolute;
+ z-index: 1000000;
+ box-sizing: border-box;
+ border: $border-focus;
+ border-top: $border;
+ border-radius: 0 0 $border-radius $border-radius;
+ background-color: rgb(255, 255, 255);
+
+ .s25-body {
+ padding-top: 4px;
+ padding-bottom: 4px;
+ }
+
+ .s25-search-results {
+ width: 100%;
+ overflow-y: auto;
+ position: relative;
+ box-sizing: border-box;
+ }
+
+ .s25-options {
+ width: 100%;
+
+ .s25-item {
+ display: block;
+ width: 100%;
+ box-sizing: border-box;
+ cursor: pointer;
+
+ background-color: transparent;
+ color: inherit;
+ font-size: inherit;
+ padding: $dropdown-padding;
+ }
+
+ .s25-active {
+ background-color: lighten($item-bkg-color, 50%);
+ }
+ }
+
+ .s25-no-search-results {
+ padding: $dropdown-padding;
+ }
+
+ .s25-search-results-message {
+ padding: $dropdown-padding;
+ }
+
+ .s25-search-results-loading {
+ padding: $dropdown-padding;
+ }
+}
+
+.s25-remove {
+ border: 1px solid $remove-bkg-color;
+ border-radius: 0 $border-radius $border-radius 0;
+ margin: 0;
+ padding: 0 5px;
+ outline: none;
+ background-color: #f1f1f1;
+ transition: 0.2s, width 0.2s ease-out;
+ cursor: pointer;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ overflow: hidden;
+
+ span svg {
+ fill: #ccc;
+ transition: fill 0.2s;
+ }
+
+ &:not(:disabled) {
+ background-color: $remove-bkg-color;
+
+ &:hover {
+ background-color: darken($remove-bkg-color, 5%);
+ }
+
+ span svg {
+ fill: $item-bkg-color;
+ }
+ }
+
+ &:not(.s25-offscreen) {
+ width: 30px;
+ }
+
+ &:focus {
+ border: $border-focus;
+ }
+
+ &:disabled {
+ cursor: default;
+ }
+
+ &:active {
+ border: 0px;
+ }
+}
+
+.s25-remove.s25-offscreen ~ .s25-toggle {
+ right: 0;
+}
+
+.s25-placeholder {
+ color: #ccc;
+}
diff --git a/control/src/single-select.tsx b/control/src/single-select.tsx
new file mode 100644
index 00000000..42b26700
--- /dev/null
+++ b/control/src/single-select.tsx
@@ -0,0 +1,368 @@
+import { createRef, Fragment, h, RefObject } from 'preact';
+import {
+ AbstractSelect,
+ DEFAULT_PROPS as ABSTRACT_DEFAULT_PROPS,
+ Props as AbstractSelectProps,
+ State as AbstractSelectState
+} from './abstract-select';
+import * as announce from './announce';
+import { Dropdown } from './dropdown';
+import { Remove, Toggle } from './icons';
+import { ResultList } from './result-list';
+import { style } from './style';
+import { cn, DeepPartial, extend, Key, scope } from './util';
+
+const forceImportOfH = h;
+
+export interface Props extends AbstractSelectProps {
+ value: any;
+ label: string;
+ comboboxLabel: string;
+ onChange: (value: any) => void;
+ allowClear?: boolean;
+ placeholder?: string;
+}
+
+interface State extends AbstractSelectState {
+ value: any;
+}
+
+const DEFAULT_PROPS = extend({}, ABSTRACT_DEFAULT_PROPS, { allowClear: false });
+
+export class SingleSelect extends AbstractSelect {
+ private containerRef: RefObject;
+ private dropdownRef: RefObject;
+ private bodyRef: RefObject;
+ private searchRef: RefObject;
+ private valueRef: RefObject;
+
+ public static defaultProps = DEFAULT_PROPS;
+
+ constructor(props) {
+ super(props);
+
+ this.searchRef = createRef();
+ this.bodyRef = createRef();
+ this.containerRef = createRef();
+ this.dropdownRef = createRef();
+ this.valueRef = createRef();
+
+ this.state = extend(this.state, { value: this.props.value });
+ }
+
+ public componentWillMount() {
+ announce.initialize();
+ }
+
+ public render(props, state) {
+ const { minimumCharacters, tabIndex, label, allowClear, placeholder } = props;
+ const { value, open, loading, focused, search, results } = state;
+
+ let classes = cn(style.control, style.single, { [style.open]: open }, { [style.focused]: focused });
+ if (props.containerClass && props.containerClass.length > 0) {
+ classes += ' ' + props.containerClass;
+ }
+ const resultsDomId = this.namespace + '-results';
+ const optionDomId = this.namespace + '-val';
+ const resultsNamespace = this.namespace + '-res-';
+ const dictionary = this.dictionary;
+ const showPlaceholder = !value && placeholder && placeholder.length > 0;
+ const placeholderDomId = this.namespace + '-placeholder';
+ return (
+
+
+
+
+ {value && (
+
+
{this.renderValue(value)}
+
+ )}
+ {showPlaceholder && (
+
+ {placeholder}
+
+ )}
+
+ {scope(() => {
+ const disabled = !value;
+ const clazz = cn(style.remove, { [style.offscreen]: !allowClear });
+ return (
+
+
+
+
+
+ );
+ })}
+
+
+
+
+
+ {open && (
+
+
+ = 0 ? resultsNamespace + results.active : undefined
+ }
+ aria-busy={loading}
+ onInput={this.onSearchInput}
+ onKeyDown={this.onSearchKeyDown}
+ onFocus={this.onSearchFocus}
+ />
+
+
+
+ )}
+
+ );
+ }
+
+ public componentDidMount() {
+ const css = this.props.containerStyle;
+ if (css && css.length > 0) {
+ this.containerRef.current!.setAttribute('style', css);
+ }
+ }
+
+ private onLoadMoreResults = () => {
+ this.loadMore();
+ };
+
+ public onFocusIn = (event: FocusEvent) => {
+ this.updateState({ focused: true });
+
+ const { openOnFocus } = this.props;
+ const { open } = this.state;
+ if (!open && openOnFocus && this.searchRef.current !== document.activeElement) {
+ this.open();
+ }
+ };
+
+ public onFocusOut = (event: FocusEvent) => {
+ const receiver = event.relatedTarget as Node;
+ const container = this.containerRef.current;
+ const dropdown = this.dropdownRef.current;
+ const search = this.searchRef.current;
+
+ const focused =
+ container.contains(receiver) ||
+ (dropdown && (dropdown === receiver || dropdown.contains(receiver))) ||
+ receiver === search;
+
+ if (this.state.focused !== focused) {
+ this.updateState({
+ focused
+ });
+ }
+ if (!focused) {
+ this.closeIfOpen();
+ }
+ };
+
+ public closeIfOpen() {
+ if (this.state.open) {
+ this.close();
+ }
+ }
+
+ public close = (state?: DeepPartial) => {
+ const control = this;
+ control.valueRef.current!.focus();
+ this.updateState([
+ state,
+ {
+ open: false,
+ results: { results: null },
+ search: ''
+ }
+ ]);
+ };
+
+ private getValueAsArray() {
+ return this.state.value ? [this.state.value] : [];
+ }
+
+ private onContainerMouseDown = (event: MouseEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+ if (this.state.open) {
+ this.close();
+ } else {
+ this.open();
+ }
+ };
+
+ private open(query: string = '') {
+ this.search(query, this.getValueAsArray(), { open: true }, () => {
+ this.searchRef.current.focus();
+ });
+ }
+
+ private onSearchFocus = (event: FocusEvent) => {
+ this.updateState({ focused: true });
+ };
+
+ private onSearchInput = (event: Event) => {
+ const value = (event.target as HTMLInputElement).value;
+ this.search(value, this.getValueAsArray());
+ };
+
+ private onClearFocus = (event: FocusEvent) => {
+ this.closeIfOpen();
+ };
+
+ private onClearClick = (event: Event) => {
+ this.selectResult(undefined);
+ event.preventDefault();
+ event.stopPropagation();
+ };
+
+ private onClearMouseDown = (event: Event) => {
+ event.stopPropagation();
+ event.preventDefault();
+ };
+
+ public onSearchKeyDown = (event: KeyboardEvent) => {
+ if (this.handleResultNavigationKeyDown(event)) {
+ return;
+ }
+
+ const { open } = this.state;
+
+ if (open && this.hasSearchResults) {
+ switch (event.key) {
+ case Key.Enter:
+ this.selectActiveResult();
+ event.preventDefault();
+ event.stopPropagation();
+ break;
+ case Key.Escape:
+ this.close();
+ event.preventDefault();
+ event.stopPropagation();
+ break;
+ case Key.Tab:
+ // TODO select on tab?
+ this.close();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+ };
+
+ public selectActiveResult = () => {
+ const { active } = this.state.results;
+ if (active >= 0) {
+ this.selectResult(this.getSelectedSearchResult());
+ }
+ };
+
+ public selectResult = (result: any) => {
+ const { onChange } = this.props;
+
+ this.close({ value: result });
+
+ // TODO announce?
+ // const label = this.getItemLabel(result);
+
+ onChange(result);
+ };
+
+ private onValueKeyDown = (event: KeyboardEvent) => {
+ switch (event.key) {
+ case Key.Space:
+ case Key.ArrowDown:
+ case Key.Down:
+ this.open();
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+
+ if (event.key.length === 1) {
+ // focus on search which will put the printable character into the field
+ this.open();
+ }
+ };
+
+ private onDropdownMouseDown = (event: MouseEvent) => {
+ this.searchRef.current.focus();
+ event.preventDefault();
+ event.stopPropagation();
+ };
+
+ public onResultMouseMove = (index: number, event: MouseEvent) => {
+ this.selectSearchResult(index);
+ };
+
+ public onResultClicked = (result: any, event: MouseEvent) => {
+ this.selectResult(result);
+ event.preventDefault();
+ event.stopPropagation();
+ };
+}
diff --git a/control/src/style.ts b/control/src/style.ts
new file mode 100644
index 00000000..e68f0205
--- /dev/null
+++ b/control/src/style.ts
@@ -0,0 +1,31 @@
+export const enum style {
+ control = 's25-control',
+ content = 's25-content',
+ single = 's25-single',
+ multi = 's25-multi',
+ body = 's25-body',
+ focused = 's25-focused',
+ active = 's25-active',
+ live = 's25-live',
+ multiValues = 's25-multi-values',
+ value = 's25-value',
+ item = 's25-item',
+ selected = 's25-selected',
+ toggle = 's25-toggle',
+ remove = 's25-remove',
+ open = 's25-open',
+ label = 's25-label',
+ search = 's25-search',
+ offscreen = 's25-offscreen',
+ searchContainer = 's25-search-container',
+ dropdown = 's25-dropdown',
+ searchResults = 's25-search-results',
+ options = 's25-options',
+ noSearchResults = 's25-no-search-results',
+ searchResultsMessage = 's25-search-results-message',
+ searchResultsLoading = 's25-search-results-loading',
+ searchResultsMinimumError = 's25-search-results-minimum-error',
+ hiddenAccessible = 's25-hidden-accessible',
+ hidden = 's25-hidden',
+ placeholder = 's25-placeholder'
+}
diff --git a/control/src/util.ts b/control/src/util.ts
new file mode 100644
index 00000000..657a636b
--- /dev/null
+++ b/control/src/util.ts
@@ -0,0 +1,213 @@
+import { ComponentChild } from 'preact';
+
+export function extend(...params: object[]) {
+ for (let i = 1; i < arguments.length; i++) {
+ for (const key in arguments[i]) {
+ if (arguments[i].hasOwnProperty(key)) {
+ if (typeof arguments[0][key] === 'object' && typeof arguments[i][key] === 'object') {
+ extend(arguments[0][key], arguments[i][key]);
+ } else {
+ arguments[0][key] = arguments[i][key];
+ }
+ }
+ }
+ }
+ return arguments[0];
+}
+
+export type DeepPartial = {
+ [P in keyof T]?: T[P] extends Array
+ ? Array>
+ : T[P] extends ReadonlyArray
+ ? ReadonlyArray>
+ : DeepPartial
+};
+
+export const merge = (target: T, sources: Array>): T => {
+ if (!sources.length) {
+ return target;
+ }
+ const source = sources.shift();
+ if (source === undefined) {
+ return merge(target, sources);
+ }
+
+ if (isMergebleObject(target) && isMergebleObject(source)) {
+ Object.keys(source).forEach((key: string) => {
+ if (isMergebleObject(source[key])) {
+ if (!target[key]) {
+ target[key] = {};
+ }
+ merge(target[key], [source[key]]);
+ } else {
+ target[key] = source[key];
+ }
+ });
+ }
+
+ return merge(target, sources);
+};
+
+const isObject = (item: any): boolean => {
+ return item !== null && typeof item === 'object';
+};
+
+const isMergebleObject = (item): boolean => {
+ return isObject(item) && !Array.isArray(item);
+};
+
+export function cn(...values: any) {
+ const classes: string[] = [];
+ const hasOwnProperty = {}.hasOwnProperty;
+
+ for (const value of values) {
+ if (typeof value === 'string') {
+ classes.push(value);
+ } else if (typeof value === 'object') {
+ for (const key in value as object) {
+ if (hasOwnProperty.call(value, key) && value[key]) {
+ classes.push(key);
+ }
+ }
+ }
+ }
+
+ return classes.join(' ');
+}
+
+export enum Key {
+ // https://www.w3.org/TR/uievents-key/#named-key-attribute-values
+ ArrowDown = 'ArrowDown',
+ ArrowUp = 'ArrowUp',
+ ArrowLeft = 'ArrowLeft',
+ ArrowRight = 'ArrowRight',
+ Space = ' ',
+ Enter = 'Enter',
+ Tab = 'Tab',
+ Home = 'Home',
+ End = 'End',
+ PageUp = 'PageUp',
+ PageDown = 'PageDown',
+ Backspace = 'Backspace',
+ Delete = 'Delete',
+ Clear = 'Clear',
+ Escape = 'Escape',
+ // IE 11
+ Down = 'Down',
+ Up = 'Up',
+ Spacebar = 'Spacebar',
+ Left = 'Left',
+ Right = 'Right'
+}
+
+export const uuid = (() => {
+ let counter = 0;
+ return () => 's25-' + counter++;
+})();
+
+export function throttle(delay: number, callback: () => void): () => void {
+ let timeout: number | undefined;
+ return () => {
+ if (timeout !== undefined) {
+ window.clearTimeout(timeout);
+ timeout = undefined;
+ } else {
+ timeout = window.setTimeout(() => {
+ callback();
+ timeout = undefined;
+ }, delay);
+ }
+ };
+}
+
+// @ts-ignore
+export function debounce(quiet: number, delegate: (...args: any[]) => void, that: object) {
+ const args = Array.from(arguments);
+ if (quiet <= 0) {
+ return () => {
+ delegate.apply(that, args);
+ };
+ } else {
+ let timeout: number | undefined;
+ return () => {
+ if (timeout) {
+ window.clearTimeout(timeout);
+ }
+ timeout = window.setTimeout(() => {
+ timeout = undefined;
+ delegate.apply(that, args);
+ }, quiet);
+ };
+ }
+}
+
+export function getScrollParents(el: HTMLElement): EventTarget[] {
+ const style = window.getComputedStyle(el);
+ const elementPosition = style.position;
+ if (elementPosition === 'fixed') {
+ return [el];
+ }
+
+ const parents: Array = [];
+ let parent = el.parentElement;
+
+ while (parent && parent.nodeType === 1) {
+ const css = window.getComputedStyle(parent);
+ if (/(overlay|scroll|auto)/.test(css.overflow + ' ' + css.overflowX + ' ' + css.overflowY)) {
+ if (elementPosition !== 'absolute' || ['relative', 'fixed', 'absolute'].indexOf(css.position || '') >= 0) {
+ parents.push(parent);
+ }
+ }
+ parent = parent.parentElement;
+ }
+
+ if (el.ownerDocument) {
+ parents.push(el.ownerDocument.body);
+ }
+
+ // iframe
+ if (el.ownerDocument !== document && el.ownerDocument && el.ownerDocument.defaultView) {
+ parents.push(el.ownerDocument.defaultView);
+ }
+
+ parents.push(window);
+ return parents;
+}
+
+export function calculateVerticalVisibility(
+ container: HTMLElement,
+ element: HTMLElement
+): 'hidden' | 'partial-top' | 'partial-bottom' | 'visible' {
+ const c = container.getBoundingClientRect();
+ const e = element.getBoundingClientRect();
+
+ if (e.bottom < c.top) {
+ // above the fold
+ return 'hidden';
+ }
+
+ if (e.top > c.bottom) {
+ // below the fold
+ return 'hidden';
+ }
+
+ if (e.top < c.top && e.bottom <= c.bottom) {
+ return 'partial-top';
+ }
+
+ if (e.top >= c.top && e.bottom > c.bottom) {
+ return 'partial-bottom';
+ }
+
+ return 'visible';
+}
+
+export type MouseEventListener = (event: MouseEvent) => void;
+export type KeyboardEventListener = (event: KeyboardEvent) => void;
+export type EventListener = (event: Event) => void;
+export type FocusEventListener = (event: FocusEvent) => void;
+
+/** helper that makes it easier to declare a scope inside a jsx block */
+export function scope(delegate: () => ComponentChild) {
+ return delegate();
+}
diff --git a/control/test/__snapshots__/multi-select.unit.test.tsx.snap b/control/test/__snapshots__/multi-select.unit.test.tsx.snap
new file mode 100644
index 00000000..5566f008
--- /dev/null
+++ b/control/test/__snapshots__/multi-select.unit.test.tsx.snap
@@ -0,0 +1,64 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MultiSelect renders with empty values 1`] = `
+
+
+
+
Items can be removed from this list box by selecting them and activating
+ 'Remove selected values' button. Items can be added by selecting them in
+ the adjacent combobox.
+
+
+
+
+
+
Add Value
+
+
+
+
+
+
+
+`;
+
+exports[`MultiSelect renders with values 1`] = `
+
+
+
+
Items can be removed from this list box by selecting them and activating
+ 'Remove selected values' button. Items can be added by selecting them in
+ the adjacent combobox.
+
+
+
+
+
+
+
Add Value
+
+
+
+
+
+
+
+`;
diff --git a/control/test/countries.ts b/control/test/countries.ts
new file mode 100644
index 00000000..e88387a7
--- /dev/null
+++ b/control/test/countries.ts
@@ -0,0 +1,245 @@
+export const countries = [
+ { name: 'United States', code: 'US' },
+ { name: 'Mexico', code: 'MX' },
+ { name: 'Afghanistan', code: 'AF' },
+ { name: 'Aland Islands', code: 'AX' },
+ { name: 'Albania', code: 'AL' },
+ { name: 'Algeria', code: 'DZ' },
+ { name: 'American Samoa', code: 'AS' },
+ { name: 'Andorra', code: 'AD' },
+ { name: 'Angola', code: 'AO' },
+ { name: 'Anguilla', code: 'AI' },
+ { name: 'Antarctica', code: 'AQ' },
+ { name: 'Antigua and Barbuda', code: 'AG' },
+ { name: 'Argentina', code: 'AR' },
+ { name: 'Armenia', code: 'AM' },
+ { name: 'Aruba', code: 'AW' },
+ { name: 'Australia', code: 'AU' },
+ { name: 'Austria', code: 'AT' },
+ { name: 'Azerbaijan', code: 'AZ' },
+ { name: 'Bahamas', code: 'BS' },
+ { name: 'Bahrain', code: 'BH' },
+ { name: 'Bangladesh', code: 'BD' },
+ { name: 'Barbados', code: 'BB' },
+ { name: 'Belarus', code: 'BY' },
+ { name: 'Belgium', code: 'BE' },
+ { name: 'Belize', code: 'BZ' },
+ { name: 'Benin', code: 'BJ' },
+ { name: 'Bermuda', code: 'BM' },
+ { name: 'Bhutan', code: 'BT' },
+ { name: 'Bolivia', code: 'BO' },
+ { name: 'Bosnia and Herzegovina', code: 'BA' },
+ { name: 'Botswana', code: 'BW' },
+ { name: 'Bouvet Island', code: 'BV' },
+ { name: 'Brazil', code: 'BR' },
+ { name: 'British Indian Ocean Territory', code: 'IO' },
+ { name: 'Brunei Darussalam', code: 'BN' },
+ { name: 'Bulgaria', code: 'BG' },
+ { name: 'Burkina Faso', code: 'BF' },
+ { name: 'Burundi', code: 'BI' },
+ { name: 'Cambodia', code: 'KH' },
+ { name: 'Cameroon', code: 'CM' },
+ { name: 'Canada', code: 'CA' },
+ { name: 'Cape Verde', code: 'CV' },
+ { name: 'Cayman Islands', code: 'KY' },
+ { name: 'Central African Republic', code: 'CF' },
+ { name: 'Chad', code: 'TD' },
+ { name: 'Chile', code: 'CL' },
+ { name: 'China', code: 'CN' },
+ { name: 'Christmas Island', code: 'CX' },
+ { name: 'Cocos (Keeling) Islands', code: 'CC' },
+ { name: 'Colombia', code: 'CO' },
+ { name: 'Comoros', code: 'KM' },
+ { name: 'Congo', code: 'CG' },
+ { name: 'Congo, The Democratic Republic of the', code: 'CD' },
+ { name: 'Cook Islands', code: 'CK' },
+ { name: 'Costa Rica', code: 'CR' },
+ { name: "Cote D'Ivoire", code: 'CI' },
+ { name: 'Croatia', code: 'HR' },
+ { name: 'Cuba', code: 'CU' },
+ { name: 'Cyprus', code: 'CY' },
+ { name: 'Czech Republic', code: 'CZ' },
+ { name: 'Denmark', code: 'DK' },
+ { name: 'Djibouti', code: 'DJ' },
+ { name: 'Dominica', code: 'DM' },
+ { name: 'Dominican Republic', code: 'DO' },
+ { name: 'Ecuador', code: 'EC' },
+ { name: 'Egypt', code: 'EG' },
+ { name: 'El Salvador', code: 'SV' },
+ { name: 'Equatorial Guinea', code: 'GQ' },
+ { name: 'Eritrea', code: 'ER' },
+ { name: 'Estonia', code: 'EE' },
+ { name: 'Ethiopia', code: 'ET' },
+ { name: 'Falkland Islands (Malvinas)', code: 'FK' },
+ { name: 'Faroe Islands', code: 'FO' },
+ { name: 'Fiji', code: 'FJ' },
+ { name: 'Finland', code: 'FI' },
+ { name: 'France', code: 'FR' },
+ { name: 'French Guiana', code: 'GF' },
+ { name: 'French Polynesia', code: 'PF' },
+ { name: 'French Southern Territories', code: 'TF' },
+ { name: 'Gabon', code: 'GA' },
+ { name: 'Gambia', code: 'GM' },
+ { name: 'Georgia', code: 'GE' },
+ { name: 'Germany', code: 'DE' },
+ { name: 'Ghana', code: 'GH' },
+ { name: 'Gibraltar', code: 'GI' },
+ { name: 'Greece', code: 'GR' },
+ { name: 'Greenland', code: 'GL' },
+ { name: 'Grenada', code: 'GD' },
+ { name: 'Guadeloupe', code: 'GP' },
+ { name: 'Guam', code: 'GU' },
+ { name: 'Guatemala', code: 'GT' },
+ { name: 'Guernsey', code: 'GG' },
+ { name: 'Guinea', code: 'GN' },
+ { name: 'Guinea-Bissau', code: 'GW' },
+ { name: 'Guyana', code: 'GY' },
+ { name: 'Haiti', code: 'HT' },
+ { name: 'Heard Island and Mcdonald Islands', code: 'HM' },
+ { name: 'Holy See (Vatican City State)', code: 'VA' },
+ { name: 'Honduras', code: 'HN' },
+ { name: 'Hong Kong', code: 'HK' },
+ { name: 'Hungary', code: 'HU' },
+ { name: 'Iceland', code: 'IS' },
+ { name: 'India', code: 'IN' },
+ { name: 'Indonesia', code: 'ID' },
+ { name: 'Iran, Islamic Republic Of', code: 'IR' },
+ { name: 'Iraq', code: 'IQ' },
+ { name: 'Ireland', code: 'IE' },
+ { name: 'Isle of Man', code: 'IM' },
+ { name: 'Israel', code: 'IL' },
+ { name: 'Italy', code: 'IT' },
+ { name: 'Jamaica', code: 'JM' },
+ { name: 'Japan', code: 'JP' },
+ { name: 'Jersey', code: 'JE' },
+ { name: 'Jordan', code: 'JO' },
+ { name: 'Kazakhstan', code: 'KZ' },
+ { name: 'Kenya', code: 'KE' },
+ { name: 'Kiribati', code: 'KI' },
+ { name: "Korea, Democratic People'S Republic of", code: 'KP' },
+ { name: 'Korea, Republic of', code: 'KR' },
+ { name: 'Kuwait', code: 'KW' },
+ { name: 'Kyrgyzstan', code: 'KG' },
+ { name: "Lao People'S Democratic Republic", code: 'LA' },
+ { name: 'Latvia', code: 'LV' },
+ { name: 'Lebanon', code: 'LB' },
+ { name: 'Lesotho', code: 'LS' },
+ { name: 'Liberia', code: 'LR' },
+ { name: 'Libyan Arab Jamahiriya', code: 'LY' },
+ { name: 'Liechtenstein', code: 'LI' },
+ { name: 'Lithuania', code: 'LT' },
+ { name: 'Luxembourg', code: 'LU' },
+ { name: 'Macao', code: 'MO' },
+ { name: 'Macedonia, The Former Yugoslav Republic of', code: 'MK' },
+ { name: 'Madagascar', code: 'MG' },
+ { name: 'Malawi', code: 'MW' },
+ { name: 'Malaysia', code: 'MY' },
+ { name: 'Maldives', code: 'MV' },
+ { name: 'Mali', code: 'ML' },
+ { name: 'Malta', code: 'MT' },
+ { name: 'Marshall Islands', code: 'MH' },
+ { name: 'Martinique', code: 'MQ' },
+ { name: 'Mauritania', code: 'MR' },
+ { name: 'Mauritius', code: 'MU' },
+ { name: 'Mayotte', code: 'YT' },
+ { name: 'Micronesia, Federated States of', code: 'FM' },
+ { name: 'Moldova, Republic of', code: 'MD' },
+ { name: 'Monaco', code: 'MC' },
+ { name: 'Mongolia', code: 'MN' },
+ { name: 'Montserrat', code: 'MS' },
+ { name: 'Morocco', code: 'MA' },
+ { name: 'Mozambique', code: 'MZ' },
+ { name: 'Myanmar', code: 'MM' },
+ { name: 'Namibia', code: 'NA' },
+ { name: 'Nauru', code: 'NR' },
+ { name: 'Nepal', code: 'NP' },
+ { name: 'Netherlands', code: 'NL' },
+ { name: 'Netherlands Antilles', code: 'AN' },
+ { name: 'New Caledonia', code: 'NC' },
+ { name: 'New Zealand', code: 'NZ' },
+ { name: 'Nicaragua', code: 'NI' },
+ { name: 'Niger', code: 'NE' },
+ { name: 'Nigeria', code: 'NG' },
+ { name: 'Niue', code: 'NU' },
+ { name: 'Norfolk Island', code: 'NF' },
+ { name: 'Northern Mariana Islands', code: 'MP' },
+ { name: 'Norway', code: 'NO' },
+ { name: 'Oman', code: 'OM' },
+ { name: 'Pakistan', code: 'PK' },
+ { name: 'Palau', code: 'PW' },
+ { name: 'Palestinian Territory, Occupied', code: 'PS' },
+ { name: 'Panama', code: 'PA' },
+ { name: 'Papua New Guinea', code: 'PG' },
+ { name: 'Paraguay', code: 'PY' },
+ { name: 'Peru', code: 'PE' },
+ { name: 'Philippines', code: 'PH' },
+ { name: 'Pitcairn', code: 'PN' },
+ { name: 'Poland', code: 'PL' },
+ { name: 'Portugal', code: 'PT' },
+ { name: 'Puerto Rico', code: 'PR' },
+ { name: 'Qatar', code: 'QA' },
+ { name: 'Reunion', code: 'RE' },
+ { name: 'Romania', code: 'RO' },
+ { name: 'Russian Federation', code: 'RU' },
+ { name: 'RWANDA', code: 'RW' },
+ { name: 'Saint Helena', code: 'SH' },
+ { name: 'Saint Kitts and Nevis', code: 'KN' },
+ { name: 'Saint Lucia', code: 'LC' },
+ { name: 'Saint Pierre and Miquelon', code: 'PM' },
+ { name: 'Saint Vincent and the Grenadines', code: 'VC' },
+ { name: 'Samoa', code: 'WS' },
+ { name: 'San Marino', code: 'SM' },
+ { name: 'Sao Tome and Principe', code: 'ST' },
+ { name: 'Saudi Arabia', code: 'SA' },
+ { name: 'Senegal', code: 'SN' },
+ { name: 'Serbia and Montenegro', code: 'CS' },
+ { name: 'Seychelles', code: 'SC' },
+ { name: 'Sierra Leone', code: 'SL' },
+ { name: 'Singapore', code: 'SG' },
+ { name: 'Slovakia', code: 'SK' },
+ { name: 'Slovenia', code: 'SI' },
+ { name: 'Solomon Islands', code: 'SB' },
+ { name: 'Somalia', code: 'SO' },
+ { name: 'South Africa', code: 'ZA' },
+ { name: 'South Georgia and the South Sandwich Islands', code: 'GS' },
+ { name: 'Spain', code: 'ES' },
+ { name: 'Sri Lanka', code: 'LK' },
+ { name: 'Sudan', code: 'SD' },
+ { name: 'Suriname', code: 'SR' },
+ { name: 'Svalbard and Jan Mayen', code: 'SJ' },
+ { name: 'Swaziland', code: 'SZ' },
+ { name: 'Sweden', code: 'SE' },
+ { name: 'Switzerland', code: 'CH' },
+ { name: 'Syrian Arab Republic', code: 'SY' },
+ { name: 'Taiwan, Province of China', code: 'TW' },
+ { name: 'Tajikistan', code: 'TJ' },
+ { name: 'Tanzania, United Republic of', code: 'TZ' },
+ { name: 'Thailand', code: 'TH' },
+ { name: 'Timor-Leste', code: 'TL' },
+ { name: 'Togo', code: 'TG' },
+ { name: 'Tokelau', code: 'TK' },
+ { name: 'Tonga', code: 'TO' },
+ { name: 'Trinidad and Tobago', code: 'TT' },
+ { name: 'Tunisia', code: 'TN' },
+ { name: 'Turkey', code: 'TR' },
+ { name: 'Turkmenistan', code: 'TM' },
+ { name: 'Turks and Caicos Islands', code: 'TC' },
+ { name: 'Tuvalu', code: 'TV' },
+ { name: 'Uganda', code: 'UG' },
+ { name: 'Ukraine', code: 'UA' },
+ { name: 'United Arab Emirates', code: 'AE' },
+ { name: 'United Kingdom', code: 'GB' },
+ { name: 'United States Minor Outlying Islands', code: 'UM' },
+ { name: 'Uruguay', code: 'UY' },
+ { name: 'Uzbekistan', code: 'UZ' },
+ { name: 'Vanuatu', code: 'VU' },
+ { name: 'Venezuela', code: 'VE' },
+ { name: 'Viet Nam', code: 'VN' },
+ { name: 'Virgin Islands, British', code: 'VG' },
+ { name: 'Virgin Islands, U.S.', code: 'VI' },
+ { name: 'Wallis and Futuna', code: 'WF' },
+ { name: 'Western Sahara', code: 'EH' },
+ { name: 'Yemen', code: 'YE' },
+ { name: 'Zambia', code: 'ZM' },
+ { name: 'Zimbabwe', code: 'ZW' }
+];
diff --git a/control/test/multi-select.unit.test.tsx b/control/test/multi-select.unit.test.tsx
new file mode 100644
index 00000000..03b65352
--- /dev/null
+++ b/control/test/multi-select.unit.test.tsx
@@ -0,0 +1,42 @@
+import { h } from 'preact';
+import { MultiSelect } from '../src/multi-select';
+import { countries } from './countries';
+import { query, shallow } from './preact-util';
+
+const forceImportOfH = h;
+
+describe('MultiSelect', () => {
+ it('renders with empty values', () => {
+ const tree = shallow(
+ {
+ /* noop */
+ }}
+ />
+ );
+ expect(tree).toMatchSnapshot();
+ });
+
+ it('renders with values', () => {
+ const tree = shallow(
+ {
+ /* noop */
+ }}
+ />
+ );
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/control/test/preact-util.ts b/control/test/preact-util.ts
new file mode 100644
index 00000000..514090c2
--- /dev/null
+++ b/control/test/preact-util.ts
@@ -0,0 +1,36 @@
+import { VNode } from 'preact';
+import { render as renderToString, shallowRender as shallowRenderToString } from 'preact-render-to-string';
+import * as pretty from 'pretty';
+import { QueryFunction } from '../src/abstract-select';
+import { countries } from './countries';
+
+export function shallow(component: VNode): string {
+ return pretty(shallowRenderToString(component), { ocd: true });
+}
+export function deep(component: VNode): string {
+ return pretty(renderToString(component), { ocd: true });
+}
+
+export const query: QueryFunction = (search, page, token) =>
+ new Promise((resolve, reject) => {
+ const results: any[] = [];
+ let count = 0;
+ const limit = 10;
+ const offset = page * limit;
+ for (const country of countries) {
+ if (country.name.toLowerCase().indexOf(search.toLowerCase()) >= 0) {
+ if (count >= offset) {
+ results.push(country);
+ }
+ count++;
+ if (count >= offset + limit) {
+ break;
+ }
+ }
+ }
+ resolve({
+ more: results.length >= limit,
+ token,
+ values: results
+ });
+ });
diff --git a/control/test/test1.int.test.ts b/control/test/test1.int.test.ts
new file mode 100644
index 00000000..a12b562c
--- /dev/null
+++ b/control/test/test1.int.test.ts
@@ -0,0 +1,9 @@
+describe('Google', () => {
+ beforeAll(async () => {
+ await page.goto('https://google.com');
+ });
+
+ it('should be titled "Google"', async () => {
+ await expect(page.title()).resolves.toMatch('Google');
+ });
+});
diff --git a/dev/countries-data.js b/dev/countries-data.js
new file mode 100644
index 00000000..528ed16c
--- /dev/null
+++ b/dev/countries-data.js
@@ -0,0 +1,246 @@
+window.select2countries = [
+ { name: 'United States', code: 'US' },
+ { name: 'Mexico', code: 'MX' },
+
+ { name: 'Afghanistan', code: 'AF' },
+ { name: 'Aland Islands', code: 'AX' },
+ { name: 'Albania', code: 'AL' },
+ { name: 'Algeria', code: 'DZ' },
+ { name: 'American Samoa', code: 'AS' },
+ { name: 'Andorra', code: 'AD' },
+ { name: 'Angola', code: 'AO' },
+ { name: 'Anguilla', code: 'AI' },
+ { name: 'Antarctica', code: 'AQ' },
+ { name: 'Antigua and Barbuda', code: 'AG' },
+ { name: 'Argentina', code: 'AR' },
+ { name: 'Armenia', code: 'AM' },
+ { name: 'Aruba', code: 'AW' },
+ { name: 'Australia', code: 'AU' },
+ { name: 'Austria', code: 'AT' },
+ { name: 'Azerbaijan', code: 'AZ' },
+ { name: 'Bahamas', code: 'BS' },
+ { name: 'Bahrain', code: 'BH' },
+ { name: 'Bangladesh', code: 'BD' },
+ { name: 'Barbados', code: 'BB' },
+ { name: 'Belarus', code: 'BY' },
+ { name: 'Belgium', code: 'BE' },
+ { name: 'Belize', code: 'BZ' },
+ { name: 'Benin', code: 'BJ' },
+ { name: 'Bermuda', code: 'BM' },
+ { name: 'Bhutan', code: 'BT' },
+ { name: 'Bolivia', code: 'BO' },
+ { name: 'Bosnia and Herzegovina', code: 'BA' },
+ { name: 'Botswana', code: 'BW' },
+ { name: 'Bouvet Island', code: 'BV' },
+ { name: 'Brazil', code: 'BR' },
+ { name: 'British Indian Ocean Territory', code: 'IO' },
+ { name: 'Brunei Darussalam', code: 'BN' },
+ { name: 'Bulgaria', code: 'BG' },
+ { name: 'Burkina Faso', code: 'BF' },
+ { name: 'Burundi', code: 'BI' },
+ { name: 'Cambodia', code: 'KH' },
+ { name: 'Cameroon', code: 'CM' },
+ { name: 'Canada', code: 'CA' },
+ { name: 'Cape Verde', code: 'CV' },
+ { name: 'Cayman Islands', code: 'KY' },
+ { name: 'Central African Republic', code: 'CF' },
+ { name: 'Chad', code: 'TD' },
+ { name: 'Chile', code: 'CL' },
+ { name: 'China', code: 'CN' },
+ { name: 'Christmas Island', code: 'CX' },
+ { name: 'Cocos (Keeling) Islands', code: 'CC' },
+ { name: 'Colombia', code: 'CO' },
+ { name: 'Comoros', code: 'KM' },
+ { name: 'Congo', code: 'CG' },
+ { name: 'Congo, The Democratic Republic of the', code: 'CD' },
+ { name: 'Cook Islands', code: 'CK' },
+ { name: 'Costa Rica', code: 'CR' },
+ { name: "Cote D'Ivoire", code: 'CI' },
+ { name: 'Croatia', code: 'HR' },
+ { name: 'Cuba', code: 'CU' },
+ { name: 'Cyprus', code: 'CY' },
+ { name: 'Czech Republic', code: 'CZ' },
+ { name: 'Denmark', code: 'DK' },
+ { name: 'Djibouti', code: 'DJ' },
+ { name: 'Dominica', code: 'DM' },
+ { name: 'Dominican Republic', code: 'DO' },
+ { name: 'Ecuador', code: 'EC' },
+ { name: 'Egypt', code: 'EG' },
+ { name: 'El Salvador', code: 'SV' },
+ { name: 'Equatorial Guinea', code: 'GQ' },
+ { name: 'Eritrea', code: 'ER' },
+ { name: 'Estonia', code: 'EE' },
+ { name: 'Ethiopia', code: 'ET' },
+ { name: 'Falkland Islands (Malvinas)', code: 'FK' },
+ { name: 'Faroe Islands', code: 'FO' },
+ { name: 'Fiji', code: 'FJ' },
+ { name: 'Finland', code: 'FI' },
+ { name: 'France', code: 'FR' },
+ { name: 'French Guiana', code: 'GF' },
+ { name: 'French Polynesia', code: 'PF' },
+ { name: 'French Southern Territories', code: 'TF' },
+ { name: 'Gabon', code: 'GA' },
+ { name: 'Gambia', code: 'GM' },
+ { name: 'Georgia', code: 'GE' },
+ { name: 'Germany', code: 'DE' },
+ { name: 'Ghana', code: 'GH' },
+ { name: 'Gibraltar', code: 'GI' },
+ { name: 'Greece', code: 'GR' },
+ { name: 'Greenland', code: 'GL' },
+ { name: 'Grenada', code: 'GD' },
+ { name: 'Guadeloupe', code: 'GP' },
+ { name: 'Guam', code: 'GU' },
+ { name: 'Guatemala', code: 'GT' },
+ { name: 'Guernsey', code: 'GG' },
+ { name: 'Guinea', code: 'GN' },
+ { name: 'Guinea-Bissau', code: 'GW' },
+ { name: 'Guyana', code: 'GY' },
+ { name: 'Haiti', code: 'HT' },
+ { name: 'Heard Island and Mcdonald Islands', code: 'HM' },
+ { name: 'Holy See (Vatican City State)', code: 'VA' },
+ { name: 'Honduras', code: 'HN' },
+ { name: 'Hong Kong', code: 'HK' },
+ { name: 'Hungary', code: 'HU' },
+ { name: 'Iceland', code: 'IS' },
+ { name: 'India', code: 'IN' },
+ { name: 'Indonesia', code: 'ID' },
+ { name: 'Iran, Islamic Republic Of', code: 'IR' },
+ { name: 'Iraq', code: 'IQ' },
+ { name: 'Ireland', code: 'IE' },
+ { name: 'Isle of Man', code: 'IM' },
+ { name: 'Israel', code: 'IL' },
+ { name: 'Italy', code: 'IT' },
+ { name: 'Jamaica', code: 'JM' },
+ { name: 'Japan', code: 'JP' },
+ { name: 'Jersey', code: 'JE' },
+ { name: 'Jordan', code: 'JO' },
+ { name: 'Kazakhstan', code: 'KZ' },
+ { name: 'Kenya', code: 'KE' },
+ { name: 'Kiribati', code: 'KI' },
+ { name: "Korea, Democratic People'S Republic of", code: 'KP' },
+ { name: 'Korea, Republic of', code: 'KR' },
+ { name: 'Kuwait', code: 'KW' },
+ { name: 'Kyrgyzstan', code: 'KG' },
+ { name: "Lao People'S Democratic Republic", code: 'LA' },
+ { name: 'Latvia', code: 'LV' },
+ { name: 'Lebanon', code: 'LB' },
+ { name: 'Lesotho', code: 'LS' },
+ { name: 'Liberia', code: 'LR' },
+ { name: 'Libyan Arab Jamahiriya', code: 'LY' },
+ { name: 'Liechtenstein', code: 'LI' },
+ { name: 'Lithuania', code: 'LT' },
+ { name: 'Luxembourg', code: 'LU' },
+ { name: 'Macao', code: 'MO' },
+ { name: 'Macedonia, The Former Yugoslav Republic of', code: 'MK' },
+ { name: 'Madagascar', code: 'MG' },
+ { name: 'Malawi', code: 'MW' },
+ { name: 'Malaysia', code: 'MY' },
+ { name: 'Maldives', code: 'MV' },
+ { name: 'Mali', code: 'ML' },
+ { name: 'Malta', code: 'MT' },
+ { name: 'Marshall Islands', code: 'MH' },
+ { name: 'Martinique', code: 'MQ' },
+ { name: 'Mauritania', code: 'MR' },
+ { name: 'Mauritius', code: 'MU' },
+ { name: 'Mayotte', code: 'YT' },
+ { name: 'Micronesia, Federated States of', code: 'FM' },
+ { name: 'Moldova, Republic of', code: 'MD' },
+ { name: 'Monaco', code: 'MC' },
+ { name: 'Mongolia', code: 'MN' },
+ { name: 'Montserrat', code: 'MS' },
+ { name: 'Morocco', code: 'MA' },
+ { name: 'Mozambique', code: 'MZ' },
+ { name: 'Myanmar', code: 'MM' },
+ { name: 'Namibia', code: 'NA' },
+ { name: 'Nauru', code: 'NR' },
+ { name: 'Nepal', code: 'NP' },
+ { name: 'Netherlands', code: 'NL' },
+ { name: 'Netherlands Antilles', code: 'AN' },
+ { name: 'New Caledonia', code: 'NC' },
+ { name: 'New Zealand', code: 'NZ' },
+ { name: 'Nicaragua', code: 'NI' },
+ { name: 'Niger', code: 'NE' },
+ { name: 'Nigeria', code: 'NG' },
+ { name: 'Niue', code: 'NU' },
+ { name: 'Norfolk Island', code: 'NF' },
+ { name: 'Northern Mariana Islands', code: 'MP' },
+ { name: 'Norway', code: 'NO' },
+ { name: 'Oman', code: 'OM' },
+ { name: 'Pakistan', code: 'PK' },
+ { name: 'Palau', code: 'PW' },
+ { name: 'Palestinian Territory, Occupied', code: 'PS' },
+ { name: 'Panama', code: 'PA' },
+ { name: 'Papua New Guinea', code: 'PG' },
+ { name: 'Paraguay', code: 'PY' },
+ { name: 'Peru', code: 'PE' },
+ { name: 'Philippines', code: 'PH' },
+ { name: 'Pitcairn', code: 'PN' },
+ { name: 'Poland', code: 'PL' },
+ { name: 'Portugal', code: 'PT' },
+ { name: 'Puerto Rico', code: 'PR' },
+ { name: 'Qatar', code: 'QA' },
+ { name: 'Reunion', code: 'RE' },
+ { name: 'Romania', code: 'RO' },
+ { name: 'Russian Federation', code: 'RU' },
+ { name: 'RWANDA', code: 'RW' },
+ { name: 'Saint Helena', code: 'SH' },
+ { name: 'Saint Kitts and Nevis', code: 'KN' },
+ { name: 'Saint Lucia', code: 'LC' },
+ { name: 'Saint Pierre and Miquelon', code: 'PM' },
+ { name: 'Saint Vincent and the Grenadines', code: 'VC' },
+ { name: 'Samoa', code: 'WS' },
+ { name: 'San Marino', code: 'SM' },
+ { name: 'Sao Tome and Principe', code: 'ST' },
+ { name: 'Saudi Arabia', code: 'SA' },
+ { name: 'Senegal', code: 'SN' },
+ { name: 'Serbia and Montenegro', code: 'CS' },
+ { name: 'Seychelles', code: 'SC' },
+ { name: 'Sierra Leone', code: 'SL' },
+ { name: 'Singapore', code: 'SG' },
+ { name: 'Slovakia', code: 'SK' },
+ { name: 'Slovenia', code: 'SI' },
+ { name: 'Solomon Islands', code: 'SB' },
+ { name: 'Somalia', code: 'SO' },
+ { name: 'South Africa', code: 'ZA' },
+ { name: 'South Georgia and the South Sandwich Islands', code: 'GS' },
+ { name: 'Spain', code: 'ES' },
+ { name: 'Sri Lanka', code: 'LK' },
+ { name: 'Sudan', code: 'SD' },
+ { name: 'Suriname', code: 'SR' },
+ { name: 'Svalbard and Jan Mayen', code: 'SJ' },
+ { name: 'Swaziland', code: 'SZ' },
+ { name: 'Sweden', code: 'SE' },
+ { name: 'Switzerland', code: 'CH' },
+ { name: 'Syrian Arab Republic', code: 'SY' },
+ { name: 'Taiwan, Province of China', code: 'TW' },
+ { name: 'Tajikistan', code: 'TJ' },
+ { name: 'Tanzania, United Republic of', code: 'TZ' },
+ { name: 'Thailand', code: 'TH' },
+ { name: 'Timor-Leste', code: 'TL' },
+ { name: 'Togo', code: 'TG' },
+ { name: 'Tokelau', code: 'TK' },
+ { name: 'Tonga', code: 'TO' },
+ { name: 'Trinidad and Tobago', code: 'TT' },
+ { name: 'Tunisia', code: 'TN' },
+ { name: 'Turkey', code: 'TR' },
+ { name: 'Turkmenistan', code: 'TM' },
+ { name: 'Turks and Caicos Islands', code: 'TC' },
+ { name: 'Tuvalu', code: 'TV' },
+ { name: 'Uganda', code: 'UG' },
+ { name: 'Ukraine', code: 'UA' },
+ { name: 'United Arab Emirates', code: 'AE' },
+ { name: 'United Kingdom', code: 'GB' },
+ { name: 'United States Minor Outlying Islands', code: 'UM' },
+ { name: 'Uruguay', code: 'UY' },
+ { name: 'Uzbekistan', code: 'UZ' },
+ { name: 'Vanuatu', code: 'VU' },
+ { name: 'Venezuela', code: 'VE' },
+ { name: 'Viet Nam', code: 'VN' },
+ { name: 'Virgin Islands, British', code: 'VG' },
+ { name: 'Virgin Islands, U.S.', code: 'VI' },
+ { name: 'Wallis and Futuna', code: 'WF' },
+ { name: 'Western Sahara', code: 'EH' },
+ { name: 'Yemen', code: 'YE' },
+ { name: 'Zambia', code: 'ZM' },
+ { name: 'Zimbabwe', code: 'ZW' }
+];
diff --git a/dev/dist/countries-data.fd391f61.js b/dev/dist/countries-data.fd391f61.js
new file mode 100644
index 00000000..fc687e69
--- /dev/null
+++ b/dev/dist/countries-data.fd391f61.js
@@ -0,0 +1,1328 @@
+// modules are defined as an array
+// [ module function, map of requires ]
+//
+// map of requires is short require name -> numeric require
+//
+// anything defined in a previous bundle is accessed via the
+// orig method which is the require for previous bundles
+parcelRequire = (function(modules, cache, entry, globalName) {
+ // Save the require from previous bundle to this closure if any
+ var previousRequire = typeof parcelRequire === 'function' && parcelRequire;
+ var nodeRequire = typeof require === 'function' && require;
+
+ function newRequire(name, jumped) {
+ if (!cache[name]) {
+ if (!modules[name]) {
+ // if we cannot find the module within our internal map or
+ // cache jump to the current global require ie. the last bundle
+ // that was added to the page.
+ var currentRequire = typeof parcelRequire === 'function' && parcelRequire;
+ if (!jumped && currentRequire) {
+ return currentRequire(name, true);
+ }
+
+ // If there are other bundles on this page the require from the
+ // previous one is saved to 'previousRequire'. Repeat this as
+ // many times as there are bundles until the module is found or
+ // we exhaust the require chain.
+ if (previousRequire) {
+ return previousRequire(name, true);
+ }
+
+ // Try the node require function if it exists.
+ if (nodeRequire && typeof name === 'string') {
+ return nodeRequire(name);
+ }
+
+ var err = new Error("Cannot find module '" + name + "'");
+ err.code = 'MODULE_NOT_FOUND';
+ throw err;
+ }
+
+ localRequire.resolve = resolve;
+ localRequire.cache = {};
+
+ var module = (cache[name] = new newRequire.Module(name));
+
+ modules[name][0].call(module.exports, localRequire, module, module.exports, this);
+ }
+
+ return cache[name].exports;
+
+ function localRequire(x) {
+ return newRequire(localRequire.resolve(x));
+ }
+
+ function resolve(x) {
+ return modules[name][1][x] || x;
+ }
+ }
+
+ function Module(moduleName) {
+ this.id = moduleName;
+ this.bundle = newRequire;
+ this.exports = {};
+ }
+
+ newRequire.isParcelRequire = true;
+ newRequire.Module = Module;
+ newRequire.modules = modules;
+ newRequire.cache = cache;
+ newRequire.parent = previousRequire;
+ newRequire.register = function(id, exports) {
+ modules[id] = [
+ function(require, module) {
+ module.exports = exports;
+ },
+ {}
+ ];
+ };
+
+ var error;
+ for (var i = 0; i < entry.length; i++) {
+ try {
+ newRequire(entry[i]);
+ } catch (e) {
+ // Save first error but execute all entries
+ if (!error) {
+ error = e;
+ }
+ }
+ }
+
+ if (entry.length) {
+ // Expose entry point to Node, AMD or browser globals
+ // Based on https://github.com/ForbesLindesay/umd/blob/master/template.js
+ var mainExports = newRequire(entry[entry.length - 1]);
+
+ // CommonJS
+ if (typeof exports === 'object' && typeof module !== 'undefined') {
+ module.exports = mainExports;
+
+ // RequireJS
+ } else if (typeof define === 'function' && define.amd) {
+ define(function() {
+ return mainExports;
+ });
+
+ //
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Select Single
+
+ first
+ second
+ third
+ fourth
+
+
+
+ Select Multiple
+
+ first
+ second
+ third
+ fourth
+
+
+
+
+
+
+