├── .achecker.yml ├── .config ├── babel.config.json ├── eleventy.csp.js ├── eleventy.js ├── karma.conf.js ├── rollup.config.js ├── rollup.docs.js └── tsconfig.json ├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── eleventy.yml │ └── stylelint.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .stylelintrc.json ├── .travis.yml ├── CHANGELOG ├── Gruntfile.js ├── LICENSE ├── README.md ├── composer.json ├── doc_src ├── css │ ├── bootstrap.scss │ ├── stylesheet.scss │ └── variables.scss ├── data │ └── helpers.js ├── img │ └── peopleforce.png ├── includes │ ├── demo.njk │ ├── layout_default.njk │ ├── layout_esm.njk │ ├── layout_plugin.njk │ ├── layout_redirect.njk │ ├── macro_config.njk │ └── nav.njk ├── js │ ├── index.js │ ├── jquery.min.js │ ├── jqueryui.js │ └── states.json └── pages │ ├── docs │ ├── api.njk │ ├── contribute.md │ ├── events.md │ ├── index.md │ ├── migration.md │ ├── plugins.md │ └── selectize.js.md │ ├── examples │ ├── api.njk │ ├── contacts.njk │ ├── create-filter.njk │ ├── customization.njk │ ├── events.njk │ ├── i18n.njk │ ├── index.njk │ ├── lock.njk │ ├── optgroups.njk │ ├── options.njk │ ├── performance.njk │ ├── plugins.njk │ ├── remote.njk │ ├── required.njk │ ├── rtl.njk │ ├── styling.njk │ └── validation.njk │ ├── index.njk │ ├── pages.json │ ├── plugins │ ├── caret-position.njk │ ├── change-listener.njk │ ├── checkbox-options.njk │ ├── clear-button.njk │ ├── drag-drop.njk │ ├── dropdown-header.njk │ ├── dropdown-input.njk │ ├── index.njk │ ├── input-autogrow.njk │ ├── no-active-items.njk │ ├── no-backspace-delete.njk │ ├── optgroup-columns.njk │ ├── plugins.json │ ├── remove-button.njk │ ├── restore-on-backspace.njk │ └── virtual_scroll.njk │ ├── robots.njk │ └── sitemap.njk ├── package-lock.json ├── package.json ├── release.sh ├── src ├── constants.ts ├── contrib │ ├── highlight.ts │ ├── microevent.ts │ └── microplugin.ts ├── defaults.ts ├── getSettings.ts ├── plugins │ ├── caret_position │ │ └── plugin.ts │ ├── change_listener │ │ └── plugin.ts │ ├── checkbox_options │ │ ├── plugin.scss │ │ └── plugin.ts │ ├── clear_button │ │ ├── plugin.scss │ │ ├── plugin.ts │ │ └── types.ts │ ├── drag_drop │ │ ├── plugin.scss │ │ └── plugin.ts │ ├── dropdown_header │ │ ├── plugin.scss │ │ ├── plugin.ts │ │ └── types.ts │ ├── dropdown_input │ │ ├── plugin.scss │ │ └── plugin.ts │ ├── input_autogrow │ │ ├── plugin.scss │ │ └── plugin.ts │ ├── no_active_items │ │ └── plugin.ts │ ├── no_backspace_delete │ │ └── plugin.ts │ ├── optgroup_columns │ │ ├── plugin.scss │ │ └── plugin.ts │ ├── remove_button │ │ ├── plugin.scss │ │ ├── plugin.ts │ │ └── types.ts │ ├── restore_on_backspace │ │ └── plugin.ts │ └── virtual_scroll │ │ └── plugin.ts ├── scss │ ├── -tom-select.bootstrap4.scss │ ├── -tom-select.bootstrap5.scss │ ├── _dropdown.scss │ ├── _items.scss │ ├── tom-select.bootstrap4.scss │ ├── tom-select.bootstrap5.scss │ ├── tom-select.default.scss │ └── tom-select.scss ├── tom-select.complete.ts ├── tom-select.popular.ts ├── tom-select.ts ├── types │ ├── core.ts │ ├── index.ts │ └── settings.ts ├── utils.ts └── vanilla.ts └── test ├── html ├── bootstrap-dropdown.html ├── bootstrap.bundle.js ├── tab.html └── virtual-scroll.html ├── support ├── async.js └── base.js └── tests ├── a11y.js ├── api.js ├── config-control-input.js ├── config-duplicates.js ├── config-hidePlaceholder.js ├── config-load.js ├── config.js ├── esm-module.js ├── events.js ├── events_dom.js ├── interaction.js ├── optgroups.js ├── plugins ├── caret_position.js ├── change_listener.js ├── checkbox_options.js ├── clear_button.js ├── dropdown_header.js ├── dropdown_input.js ├── input_autogrow.js ├── invalid_plugin.js ├── no_active_items.js ├── no_backspace_delete.js ├── optgroup_columns.js ├── remove_button.js ├── restore_on_backspace.js └── virtual_scroll.js ├── setup.js ├── validation.js └── xss.js /.achecker.yml: -------------------------------------------------------------------------------- 1 | # optional - Specify the rule archive 2 | # i.e. For march rule archive use ruleArchive: 2017MayDeploy 3 | # Default: latest 4 | # Refer to README.md FAQ section below to get the rule archive ID. 5 | ruleArchive: latest 6 | 7 | # optional - Specify one or many policies to scan. 8 | # i.e. For one policy use policies: IBM_Accessibility_2017_02 9 | # i.e. Multiple policies: IBM_Accessibility_2017_02,IBM_Accessibility_BETA or refer to below as a list 10 | # Default: null (all policies) 11 | # Refer to README.md FAQ section below to get the policy ID. 12 | policies: 13 | - IBM_Accessibility 14 | 15 | # optional - Specify one or many violation levels on which to fail the test 16 | # i.e. If specified violation then the testcase will only fail if 17 | # a violation is found during the scan. 18 | # i.e. failLevels: violation 19 | # i.e. failLevels: violation,potential violation or refer to below as a list 20 | # Default: violation, potentialviolation 21 | failLevels: 22 | - violation 23 | 24 | # optional - Specify one or many violation levels which should be reported 25 | # i.e. If specified violation then in the report it would only contain 26 | # results which are level of violation. 27 | # i.e. reportLevels: violation 28 | # i.e. reportLevels: violation,potentialviolation or refer to below as a list 29 | # Default: violation, potentialviolation, recommendation, potentialrecommendation, manual 30 | reportLevels: 31 | - violation 32 | - potentialviolation 33 | - recommendation 34 | - potentialrecommendation 35 | - manual 36 | 37 | # Optional - In what format types the results should be output in (json, html) 38 | # Default: json 39 | outputFormat: 40 | - json 41 | 42 | # Optional - Specify labels that you would like associated to your scan 43 | # 44 | # i.e. 45 | # label: Firefox,master,V12,Linux 46 | # label: 47 | # - Firefox 48 | # - master 49 | # - V12 50 | # - Linux 51 | # Default: N/A 52 | label: 53 | - master 54 | 55 | # optional - Where the scan results should be saved. 56 | # Default: results 57 | outputFolder: x-a11y 58 | -------------------------------------------------------------------------------- /.config/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": 3 | [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | loose: true, 8 | bugfixes: true, 9 | modules: false 10 | } 11 | ], 12 | "@babel/typescript" 13 | ], 14 | "plugins": [ 15 | ["@babel/plugin-proposal-class-properties", { "loose": true }] 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.config/eleventy.csp.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Add Content-Security-Policy to each page 4 | * 5 | * 6 | * based on csp functionality in https://github.com/google/eleventy-high-performance-blog 7 | * 8 | */ 9 | module.exports = function( eleventyConfig, config = {} ) { 10 | 11 | const crypto = require('crypto'); 12 | const { JSDOM } = require('jsdom'); 13 | var csp = config.csp; 14 | 15 | 16 | // add meta tag to each html page 17 | eleventyConfig.addTransform("csp", async (rawContent, outputPath) => { 18 | 19 | if( outputPath && outputPath.endsWith(".html") ){ 20 | 21 | var this_csp = Object.assign({}, csp); 22 | 23 | const dom = new JSDOM(rawContent); 24 | let hashes = await CSPHashes(dom,'script'); 25 | this_csp['script-src'] = this_csp['script-src'].concat( hashes ); 26 | this_csp['style-src'] = this_csp['style-src'].concat( await CSPHashes(dom,'style') ); 27 | 28 | 29 | var meta_tag = dom.window.document.createElement('meta'); 30 | meta_tag.setAttribute('http-equiv','Content-Security-Policy'); 31 | meta_tag.setAttribute('content', serializeCSP(this_csp) ); 32 | dom.window.document.querySelector('head').append(meta_tag); 33 | 34 | return dom.serialize(); 35 | 36 | } 37 | 38 | return rawContent; 39 | }); 40 | 41 | // create array of sha256 hashes for all inline content matching type 42 | async function CSPHashes(dom,type){ 43 | var hashes = []; 44 | 45 | dom.window.document.querySelectorAll(type).forEach( (element) => { 46 | if( element.hasAttribute('csp-hash') ){ 47 | const hash = 'sha256-'+crypto.createHash('sha256').update(element.textContent).digest('base64') 48 | element.removeAttribute('csp-hash'); 49 | hashes.push(`'${hash}'`); 50 | }else if( element.textContent.trim() == '' && element.getAttribute('src') === null ){ 51 | element.remove(); 52 | } 53 | }); 54 | 55 | return hashes; 56 | } 57 | 58 | function serializeCSP(csp) { 59 | var policy = []; 60 | for(let src in csp){ 61 | policy.push(src + " "+csp[src].join(" ")); 62 | } 63 | return policy.join(";"); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /.config/eleventy.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = function(eleventyConfig) { 4 | // Aliases are in relation to the _includes folder 5 | eleventyConfig.addLayoutAlias('about', 'layouts/about.html'); 6 | eleventyConfig.addPassthroughCopy({'doc_src/css':'css'}); 7 | eleventyConfig.addPassthroughCopy({'doc_src/js':'js'}); 8 | eleventyConfig.addPassthroughCopy({'build/js':'js'}); 9 | eleventyConfig.addPassthroughCopy({'build/css':'css'}); 10 | eleventyConfig.addPassthroughCopy({'build/esm':'esm'}); 11 | 12 | // content security policy 13 | const csp_plugin = require('./eleventy.csp.js'); 14 | eleventyConfig.addPlugin(csp_plugin,{ 15 | csp:{ 16 | 'default-src': ["'self'"], 17 | 'img-src': ['https://*','data:'], 18 | 'style-src': ["'self'",'unpkg.com','cdnjs.cloudflare.com','cdn.jsdelivr.net'], 19 | 'script-src': ["'self'","'unsafe-eval'",'mc.yandex.ru','cdn.jsdelivr.net'], // unsafe-eval for docsearch 20 | 'font-src': ["'self'",'cdnjs.cloudflare.com'], 21 | 'connect-src': ['api.github.com','whatcms.org','api.reddit.com','mc.yandex.ru','https://*.algolia.net','https://*.algolianet.com'], 22 | } 23 | }); 24 | 25 | // header anchors 26 | const anchors_plugin = require('@orchidjs/eleventy-plugin-ids'); 27 | eleventyConfig.addPlugin(anchors_plugin,{ 28 | prefix:'', 29 | selectors:[ 30 | '.container h1', 31 | '.container h2', 32 | '.container h3', 33 | '.container h4', 34 | '.container h5', 35 | '.container h6', 36 | '.container td:first-child', 37 | ] 38 | }); 39 | 40 | 41 | // syntax highlighting 42 | const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight"); 43 | eleventyConfig.addPlugin(syntaxHighlight); 44 | 45 | function GlobCollection(name, glob){ 46 | eleventyConfig.addCollection(name, function(collection) { 47 | return collection.getFilteredByGlob(glob) 48 | .filter(function(page){ 49 | return !page.data.exclude; 50 | }) 51 | .sort(function(a, b) { 52 | let nameA = a.data.title.toUpperCase(); 53 | let nameB = b.data.title.toUpperCase(); 54 | if (nameA < nameB) return -1; 55 | else if (nameA > nameB) return 1; 56 | else return 0; 57 | }); 58 | }); 59 | } 60 | 61 | GlobCollection('plugins','doc_src/pages/plugins/*.njk') 62 | GlobCollection('demosAlpha','doc_src/pages/examples/*.njk') 63 | 64 | // link shortcode 65 | eleventyConfig.addNunjucksShortcode('nav_link', function(item) { 66 | var title = ''; 67 | if( item.title ){ 68 | title = item.title; 69 | }else if( item.data.nav_title ){ 70 | title = item.data.nav_title; 71 | }else{ 72 | title = item.data.title; 73 | } 74 | 75 | if( this.page.url == item.url || this.page.filePathStem == item.filePathStem ){ 76 | return `${title}`; 77 | } 78 | 79 | return `${title}`; 80 | }); 81 | 82 | 83 | 84 | let markdownIt = require('markdown-it'); 85 | md = markdownIt({ 86 | html: true, 87 | breaks: false, 88 | //linkify: true 89 | }); 90 | let orig_normalizeLink = md.normalizeLink; 91 | md.normalizeLink = function(url){ 92 | 93 | // change "usage.md" to "usage" 94 | if( url.substr(-3) === '.md' ){ 95 | url = url.substr(0,url.length - 3); 96 | } 97 | 98 | // change "usage" to "../usage" 99 | if( url.indexOf(':') == -1 && url.indexOf('/') != 0 && url.indexOf('#') == -1 ){ 100 | url = '../'+url; 101 | } 102 | 103 | return orig_normalizeLink.call(this,url); 104 | } 105 | 106 | eleventyConfig.setLibrary('md', md ); 107 | 108 | return { 109 | dir: { 110 | data: '../data', // relative to input path 111 | input: 'doc_src/pages', // relative to project root 112 | output: 'build-docs', // relative to project root 113 | includes: '../includes', // relative to input path 114 | } 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /.config/rollup.docs.js: -------------------------------------------------------------------------------- 1 | import alias from '@rollup/plugin-alias'; 2 | import resolve from '@rollup/plugin-node-resolve'; // so Rollup can resolve imports without file extensions and `node_modules` 3 | import babel from '@rollup/plugin-babel'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | import path from 'path'; 6 | 7 | var configs = []; 8 | 9 | const extensions = [ 10 | '.js', '.jsx', '.ts', '.tsx', 11 | ]; 12 | 13 | var babel_config = babel({ 14 | extensions: extensions, 15 | babelHelpers: 'bundled', 16 | configFile: path.resolve(__dirname,'babel.config.json'), 17 | exclude:'node_modules/**' 18 | }); 19 | 20 | var resolve_config = resolve({ 21 | extensions: extensions, 22 | }); 23 | 24 | var terser_config = terser({ 25 | mangle: true, 26 | toplevel: true, // removes tomSelect footer 27 | format: { 28 | semicolons: false, 29 | }, 30 | }); 31 | 32 | // bootstrap tabs for docs 33 | configs.push({ 34 | input: 'doc_src/js/index.js', 35 | output: { 36 | file: path.resolve(__dirname,'../build-docs/js/index.bundle.js'), 37 | name: 'bootstrap', 38 | format: 'umd', 39 | sourcemap: true, 40 | preserveModules: false, 41 | }, 42 | plugins:[ 43 | babel_config, 44 | terser_config, 45 | alias({ 46 | entries: [ 47 | { find: '@popperjs/core', replacement: 'node_modules/@popperjs/core/dist/esm/index.js' }, 48 | ], 49 | resolve_config 50 | }) 51 | ] 52 | 53 | 54 | }); 55 | 56 | 57 | export default configs; 58 | -------------------------------------------------------------------------------- /.config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["../src/**/*"], 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "strict": true, 7 | "target": "ES6", 8 | "module": "esnext", 9 | "noUnusedLocals": true, 10 | "alwaysStrict": true, 11 | "strictNullChecks": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "allowUnreachableCode": false, 15 | "noUncheckedIndexedAccess": true, 16 | 17 | "lib": [ 18 | "ESNext", 19 | "dom" 20 | ], 21 | 22 | "declaration": true, 23 | "declarationDir": "../dist/types", 24 | "isolatedModules": true, 25 | "moduleResolution": "node" 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = tab 3 | tab_width = 4 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: tom-select 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Create an example on JSFiddle, CodePen or similar service and outline the steps for reproducing the bug. 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | - OS: [e.g. iOS, Windows] 26 | - Browser [e.g. chrome, safari] 27 | - Version [e.g. 22] 28 | - Device: [e.g. iPhone6] 29 | 30 | 31 | 14 | -------------------------------------------------------------------------------- /.github/workflows/eleventy.yml: -------------------------------------------------------------------------------- 1 | name: Build Eleventy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-20.04 11 | 12 | strategy: 13 | matrix: 14 | node-version: [14.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | 24 | - name: Install dependencies & build 25 | run: | 26 | npm install 27 | grunt 28 | grunt builddocs 29 | 30 | - name: Deploy 31 | uses: peaceiris/actions-gh-pages@v3 32 | with: 33 | publish_dir: ./build-docs 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | cname: tom-select.js.org 36 | -------------------------------------------------------------------------------- /.github/workflows/stylelint.yml: -------------------------------------------------------------------------------- 1 | name: StyleLint 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | prettier: 13 | name: StyleLint Check Action 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@master 17 | - name: Setup Node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: '16.x' 21 | - name: Get yarn cache directory path 22 | id: yarn-cache-dir-path 23 | run: echo "::set-output name=dir::$(yarn cache dir)" 24 | - uses: actions/cache@v1 25 | id: yarn-cache 26 | with: 27 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 28 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-yarn- 31 | - name: Yarn install 32 | run: yarn 33 | - name: Run Stylelint Check 34 | run: yarn stylelint 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .DAV 3 | node_modules 4 | *.log 5 | *.tmp.* 6 | *.lock 7 | build/* 8 | build-docs/* 9 | .sass-cache 10 | x-* 11 | *.err 12 | coverage/ 13 | stats/ 14 | src/tom-select.custom.ts 15 | dist 16 | /vendor/ 17 | /.idea/ 18 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run test:typescript 5 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard-scss", 4 | "stylelint-config-prettier-scss" 5 | ], 6 | 7 | "rules": { 8 | "selector-class-pattern": null, 9 | "scss/no-global-function-names": null, 10 | "scss/dollar-variable-pattern": null 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | env: 3 | matrix: 4 | - TARGET=HeadlessFirefox 5 | - TARGET=HeadlessChrome 6 | - TARGET=browserstack 7 | node_js: 8 | - 14 9 | addons: 10 | firefox: latest 11 | before_script: 12 | - npm install -g grunt-cli 13 | - npm install 14 | - grunt 15 | script: 16 | - export COMMIT_MESSAGE=$(git show -s --format=%B $TRAVIS_COMMIT | tr -d '\n') 17 | - export TRAVIS_CI=1 18 | - npm test; 19 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Version 2.2.8 (29 Sept 2023) 2 | ============================ 3 | - Correct double border issue introduced in v2.2.3 - from @craigh 4 | 5 | Version 2.2.7 (19 Sept 2023) 6 | ============================ 7 | - Merge [PR #15](https://github.com/RemoteDevForce/tom-select/pull/15) 8 | - revert part of border fix - from @MatthewKennedy 9 | 10 | Version 2.2.6 (15 Sept 2023) 11 | ============================ 12 | - Merge [PR #14](https://github.com/RemoteDevForce/tom-select/pull/14) 13 | - Fix border issue - from @MatthewKennedy 14 | - update README 15 | 16 | Version 2.2.5 (25 July 2023) 17 | ============================ 18 | - correcting release process 19 | 20 | Version 2.2.4 (21 July 2023) 21 | ============================ 22 | - correcting release process 23 | 24 | Version 2.2.3 (17 July 2023) 25 | ============================ 26 | - Merge [PR #534](https://github.com/orchidjs/tom-select/pull/534) from original repo. 27 | - Fix SCSS Warnings - from @MatthewKennedy 28 | - Merge [PR #601](https://github.com/orchidjs/tom-select/pull/601) from original repo. 29 | - Adapt Bootstrap 5 SCSS for Bootstrap 5.3+ = from @czj 30 | - Update syntax to allow SASS to compile properly from @Robert430404 31 | - Refactored the karma configs to properly load plugins locally from @Robert430404 32 | - Added package lock to pin the versions of packages from @Robert430404 33 | - Disabled a11y tests due to non-conformance from @Robert430404 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ABANDONED 7 February 2025 2 | 3 | This fork is now abandoned in favor of the original [library](https://github.com/orchidjs/tom-select) which has had several 4 | releases since this fork was created. Please switch back to the original. Thank you. 5 | 6 | # Tom Select 7 | 8 | This is a **fork** of the [original bundle](https://github.com/orchidjs/tom-select). It exists as a public service to 9 | attempt to merge and release some of the outstanding PRs from that project and improve forward-compatibility. 10 | I am not an expert in most of the tech used here - I see myself strictly as a steward. 11 | If you want something merged, you need to provide a PR and it needs to pass the pipelines. 12 | I'm happy to merge passing PRs and release when appropriate. 13 | 14 | --- 15 | 16 | Tom Select is a dynamic, framework agnostic, and lightweight (~16kb gzipped) <select> UI control. 17 | With autocomplete and native-feeling keyboard navigation, it's useful for tagging, contact lists, country selectors, and so on. 18 | Tom Select was forked from [selectize.js](https://tom-select.js.org/docs/selectize.js/) with the goal of modernizing the code base, decoupling from jQuery, and expanding functionality. 19 | 20 | 21 | ### Features 22 | 23 | - **Smart Option Searching / Ranking**
Options are efficiently scored and sorted on-the-fly (using [sifter](https://github.com/orchidjs/sifter.js)). Want to search an item's title *and* description? No problem. 24 | - **Caret between items**
Order matters sometimes. With the Caret Position Plugin, you can use the and arrow keys to move between selected items 25 | - **Select & delete multiple items at once**
Hold down command on Mac or ctrl on Windows to select more than one item to delete. 26 | - **Díåcritîçs supported**
Great for international environments. 27 | - **Item creation**
Allow users to create items on the fly (async saving is supported; the control locks until the callback is fired). 28 | - **Remote data loading**
For when you have thousands of options and want them provided by the server as the user types. 29 | - **Extensible**
[Plugin API](https://tom-select.js.org/docs/plugins/) for developing custom features (uses [microplugin](https://github.com/brianreavis/microplugin.js)). 30 | - **Accessible**, **Touch Support**, **Clean API**, ... 31 | 32 | ## Usage 33 | 34 | ```html 35 | 36 | 37 | 38 | 42 | ``` 43 | 44 | Available configuration settings are [documented here](https://tom-select.js.org/docs) 45 | 46 | ## License 47 | 48 | Copyright © 2013–2021 [Contributors](https://github.com/orchidjs/tom-select/graphs/contributors) 49 | 50 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 51 | 52 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "orchidjs/tom-select", 3 | "description": "Tom Select is a dynamic, framework agnostic, and lightweight (~16kb gzipped) 36 | 37 | 38 |
39 |
40 | {% include "nav.njk" %} 41 |
42 |
43 | 44 | 45 | {# main-col #} 46 |
47 |
48 | {% if title %} 49 |

{{ title }}

50 | {% endif %} 51 | 52 | {{ content | safe }} 53 | {% block content_foot %} 54 | {% endblock %} 55 |
56 |
57 | 58 | 59 | 66 | 67 | 68 | {# 69 | 70 | 81 | 82 | 83 | #} 84 | 85 | 86 | -------------------------------------------------------------------------------- /doc_src/includes/layout_esm.njk: -------------------------------------------------------------------------------- 1 | {% extends "layout_default.njk" %} 2 | 3 | {% block tomselectjs %} 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /doc_src/includes/layout_plugin.njk: -------------------------------------------------------------------------------- 1 | {% extends "layout_default.njk" %} 2 | 3 | 4 | {% block content_foot %} 5 |

Available Plugins

6 | {% if 'plugins' in page.filePathStem %} 7 | 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /doc_src/includes/layout_redirect.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Redirecting to: {{ destination }} 9 | 10 | 11 |
12 | Redirecting to {{ destination }} ... 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /doc_src/includes/macro_config.njk: -------------------------------------------------------------------------------- 1 | 2 | {% macro config_table(data,heading='Setting') %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% for row in data %} 11 | {{ config_row( row.name, row.desc, row.type, row.default, row.highlight) }} 12 | {% endfor %} 13 |
{{ heading }}Description
14 | {% endmacro %} 15 | 16 | {% macro config_row( setting, desc, type, default, highlight ) %} 17 | 18 | 19 | {{ setting }} 20 | 21 |
{{ desc | safe }}
22 |
23 | Type: {{ type }} 24 | Default: 25 | 26 | {% if highlight %} 27 | {% highlight "js" %} 28 | {{ default | safe }} 29 | {% endhighlight %} 30 | {% else %} 31 | {{ default }} 32 | {% endif %} 33 |
34 | 35 | 36 | 37 | {% endmacro %} 38 | -------------------------------------------------------------------------------- /doc_src/includes/nav.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | Home 4 | 5 | {# print a navigation link, add active/text-dark class if the current nav item is the current page 6 | # https://www.11ty.dev/docs/data-eleventy-supplied/#page-variable-contents 7 | #} 8 | 9 | 10 | Examples 11 | {% if 'examples' in page.filePathStem %} 12 |
13 | {% for item in collections.demosAlpha %} 14 | {% nav_link item %} 15 | {% endfor %} 16 |
17 | {% endif %} 18 | 19 | Plugins 20 | {% if '/plugins/' in page.filePathStem %} 21 |
22 | {% for item in collections.plugins %} 23 | {% nav_link item %} 24 | {% endfor %} 25 |
26 | {% endif %} 27 | 28 | 29 | Docs 30 | {% if 'docs' in page.filePathStem %} 31 |
32 | {% nav_link {url:'/docs/',title:'Usage'} %} 33 | {% nav_link {url:'/docs/api/',title:'API'} %} 34 | {% nav_link {url:'/docs/plugins/',title:'Plugins'} %} 35 | {% nav_link {url:'/docs/events/',title:'Events'} %} 36 | {% nav_link {url:'/docs/contribute/',title:'Contribute'} %} 37 | {% nav_link {url:'/docs/migration/',title:'Migration'} %} 38 | {% nav_link {url:'/docs/selectize.js/',title:'selectize.js'} %} 39 |
40 | {% endif %} 41 | 42 |
43 | 44 | GitHub 45 | NPM 46 | 47 | 48 |
49 | 50 |
51 | 52 |
53 | Edit this page on GitHub 54 |
© {{ helpers.currentYear() }} Contributors
55 |
56 | -------------------------------------------------------------------------------- /doc_src/js/states.json: -------------------------------------------------------------------------------- 1 | { 2 | "states": [ 3 | { 4 | "text": "Alabama" 5 | }, 6 | { 7 | "text": "Alaska" 8 | }, 9 | { 10 | "text": "Arizona" 11 | }, 12 | { 13 | "text": "Arkansas" 14 | }, 15 | { 16 | "text": "California" 17 | }, 18 | { 19 | "text": "Colorado" 20 | }, 21 | { 22 | "text": "Connecticut" 23 | }, 24 | { 25 | "text": "Delaware" 26 | }, 27 | { 28 | "text": "Florida" 29 | }, 30 | { 31 | "text": "Georgia" 32 | }, 33 | { 34 | "text": "Hawaii" 35 | }, 36 | { 37 | "text": "Idaho" 38 | }, 39 | { 40 | "text": "Illinois" 41 | }, 42 | { 43 | "text": "Indiana" 44 | }, 45 | { 46 | "text": "Iowa" 47 | }, 48 | { 49 | "text": "Kansas" 50 | }, 51 | { 52 | "text": "Kentucky" 53 | }, 54 | { 55 | "text": "Louisiana" 56 | }, 57 | { 58 | "text": "Maine" 59 | }, 60 | { 61 | "text": "Maryland" 62 | }, 63 | { 64 | "text": "Massachusetts" 65 | }, 66 | { 67 | "text": "Michigan" 68 | }, 69 | { 70 | "text": "Minnesota" 71 | }, 72 | { 73 | "text": "Mississippi" 74 | }, 75 | { 76 | "text": "Missouri" 77 | }, 78 | { 79 | "text": "Montana" 80 | }, 81 | { 82 | "text": "Nebraska" 83 | }, 84 | { 85 | "text": "Nevada" 86 | }, 87 | { 88 | "text": "New Hampshire" 89 | }, 90 | { 91 | "text": "New Jersey" 92 | }, 93 | { 94 | "text": "New Mexico" 95 | }, 96 | { 97 | "text": "New York" 98 | }, 99 | { 100 | "text": "North Carolina" 101 | }, 102 | { 103 | "text": "North Dakota" 104 | }, 105 | { 106 | "text": "Ohio" 107 | }, 108 | { 109 | "text": "Oklahoma" 110 | }, 111 | { 112 | "text": "Oregon" 113 | }, 114 | { 115 | "text": "Pennsylvania" 116 | }, 117 | { 118 | "text": "Rhode Island" 119 | }, 120 | { 121 | "text": "South Carolina" 122 | }, 123 | { 124 | "text": "South Dakota" 125 | }, 126 | { 127 | "text": "Tennessee" 128 | }, 129 | { 130 | "text": "Texas" 131 | }, 132 | { 133 | "text": "Utah" 134 | }, 135 | { 136 | "text": "Vermont" 137 | }, 138 | { 139 | "text": "Virginia" 140 | }, 141 | { 142 | "text": "Washington" 143 | }, 144 | { 145 | "text": "West Virginia" 146 | }, 147 | { 148 | "text": "Wisconsin" 149 | }, 150 | { 151 | "text": "Wyoming" 152 | } 153 | ] 154 | } 155 | -------------------------------------------------------------------------------- /doc_src/pages/docs/contribute.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contributing 3 | tags: docs 4 | --- 5 | 6 | 7 | ### Pull Requests 8 | 9 | If you're motivated to fix a bug or to develop a new feature, we'd love to see your code. 10 | When submitting pull requests, please remember the following: 11 | 12 | 17 | 18 | ### Build from source 19 | Compile TypeScript and SCSS in the /src directory to JavaScript and CSS in the /buid directory 20 | 21 | ```shell 22 | $ npm run build 23 | ``` 24 | 25 | ### Functional and Unit Tests 26 | Please ensure all the tests pass: 27 | 28 | ```shell 29 | $ npm test 30 | ``` 31 | 32 | ### Local Environment 33 | Runing ```npm start``` on your repo will start a web server allowing you to view a local copy of tom-select.js.org. 34 | 35 | ```shell 36 | $ npm start 37 | ``` 38 | 39 | Once started, you can run all the examples at `http://localhost:8000/`. 40 | -------------------------------------------------------------------------------- /doc_src/pages/docs/events.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Events API 3 | tags: docs 4 | --- 5 | 6 | In the [usage documentation](/docs), a few callbacks are listed that 7 | allow you to listen for certain events. Callbacks aren't always ideal though; 8 | specifically when you wish to have multiple handlers. 9 | 10 | Tom Select instances have a basic event emitter interface that mimics jQuery, Backbone.js, et al: 11 | 12 | ```js 13 | var handler = function() { /* ... */ }; 14 | var select = new TomSelect('#input-id'); 15 | select.on('event_name', handler); 16 | select.off('event_name'); 17 | select.off('event_name', handler); 18 | ``` 19 | 20 | ### List of Events 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
EventParamsDescription
"initialize"Invoked once the control is completely initialized.
"change"valueInvoked when the value of the control changes.
"focus"Invoked when the control gains focus.
"blur"Invoked when the control loses focus.
"item_add"value, itemInvoked when an item is added (i.e., when an option is selected)
"item_remove"value, $itemInvoked when an item is deselected.
"item_select"itemInvoked when an item is selected.
"clear"Invoked when the control is manually cleared via the clear() method.
"option_add"value, dataInvoked when a new option is added to the available options list.
"option_remove"valueInvoked when an option is removed from the available options.
"option_clear"Invoked when all options are removed from the control.
"optgroup_add"id, dataInvoked when a new option is added to the available options list.
"optgroup_remove"idInvoked when an option group is removed.
"optgroup_clear"Invoked when all option groups are removed.
"dropdown_open"dropdownInvoked when the dropdown opens.
"dropdown_close"dropdownInvoked when the dropdown closes.
"type"strInvoked when the user types while filtering options.
"load"dataInvoked when new options have been loaded and added to the control (via the load option or load API method).
"destroy"Invoked right before the control is destroyed.
126 | -------------------------------------------------------------------------------- /doc_src/pages/docs/migration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Migrating to v2 3 | tags: docs 4 | --- 5 | 6 | Review changes to the Tom Select API to help you migrate from v1 to v2. 7 | 8 | ## Core 9 | * Caret position functionality (using and arrow keys to move between selected items) moved to caret_position plugin 10 | * ```addOption()``` no longer treats new options as "user created" (see persist option) by default. Use ```addOption(option, true)``` for registering user created options. 11 | * Closing control via esc key, enter, etc no longer blurs focus to maintain keyboard control 12 | * Added sync() method 13 | * Original <input> or <select> element uses 'hidden-accessible' styling instead of 'hidden' 14 | * controlInput=null instead of controlInput='<input>' for hidden control input 15 | * Deprecated isInvalid. Use isValid instead 16 | * Removed support for ```querySelector('option[selected]')```. Use ```querySelector('option:checked')``` instead 17 | * Removed renderCache 18 | 19 | ## CSS 20 | * Renamed ```.ts-control``` to ```.ts-wrapper``` to align css class with name in JavaScript 21 | * Renamed ```.ts-input``` to ```.ts-control``` to align css class with name in JavaScript 22 | * Multiple CSS classes are now toggled on the wrapper element instead of the control element: ```.focus```, ```.disabled```, ```.required```, ```.invalid```, ```.locked```, ```.full```, ```.not-full```, ```.input-active```, ```.dropdown-active```, ```.has-options```, ```.has-items``` 23 | * Removed bootstrap3 style 24 | 25 | ## Settings 26 | * ```copyClassesToDropdown``` defaults to false 27 | -------------------------------------------------------------------------------- /doc_src/pages/docs/selectize.js.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: selectize.js 3 | tags: docs 4 | --- 5 | 6 |
7 | Tom Select was forked from selectize.js with four main objectives: modernizing the code base, decoupling from jQuery, expanding functionality, and addressing issue backlogs. 8 |
9 | 10 | ## Highlights as of v1.1.0 11 | 12 | ### New 13 | - support for external control input 14 | - dropdownParent other than 'body' 15 | - no_backspace_delete plugin 16 | - framework agnostic design (works without jQuery or any other JavaScript framework) 17 | - improved keyboard control of selected items 18 | - improved option cache to reduce dom manipulation during searches 19 | - animated scrolling with css instead of JavaScript 20 | - improved ctrl/shift/cmd key detection 21 | - autogrow functionality moved to input_autogrow plugin 22 | - [integrated plugin hooks](plugins.md) 23 | 24 | 25 | ### Fixed 26 | - [#1363](https://github.com/selectize/selectize.js/issues/1363) Autofill disable possibility 27 | - [#1447](https://github.com/selectize/selectize.js/issues/1447) Enhancement - dropdownParent 28 | - [#1279](https://github.com/selectize/selectize.js/issues/1279) Adding ability to use load to init opt groups 29 | - [#838](https://github.com/selectize/selectize.js/issues/838) Add option to disable delete on backspace (no_backspace_delete plugin) 30 | - [#239](https://github.com/selectize/selectize.js/issues/239) Preserve custom HTML5 data attributes 31 | - [#1128](https://github.com/selectize/selectize.js/issues/1128) Duplicated options in different optgroups doesn't render correctly 32 | - [#129](https://github.com/selectize/selectize.js/issues/129) Allow duplicate values in an input 33 | - [#470](https://github.com/selectize/selectize.js/issues/470) "No results found" message 34 | - [#999](https://github.com/selectize/selectize.js/issues/999) Don't clear the text box value on blur 35 | - [#1104](https://github.com/selectize/selectize.js/issues/1104) Replace values in single-item selection 36 | - [#1132](https://github.com/selectize/selectize.js/issues/1132) Can't enter 'ą' character in tags mode 37 | - [#102](https://github.com/selectize/selectize.js/issues/102) Listen to original select changes (via 'change_listener' plugin) 38 | - [#905](https://github.com/selectize/selectize.js/issues/905) Support for Bootstrap 4 39 | 40 | 41 | ### Breaking Changes 42 | - .ts-* css class names instead of .selectize-* (customizable with scss & js) 43 | - scss instead of less 44 | - dataAttr defaults to null instead of "data-data" 45 | - options must be appended to optgroup within custom optgroup template 46 | - removed support for older browsers including IE11 47 | 48 | 49 | ### Development Environment 50 | - code converted to TypeScript (Tom Select 1.1+) 51 | - compiled with Babel 52 | - bundled with rollup.js 53 | - examples and documentation generated using 11ty 54 | - tests run on Browserstack 55 | -------------------------------------------------------------------------------- /doc_src/pages/examples/api.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: JavaScript API Examples 3 | nav_title: JavaScript API 4 | tags: demo 5 | --- 6 | 7 | {% from "demo.njk" import demo %} 8 | 9 | {% set label %} 10 | 13 |

Examples of how to interact with the control programmatically.

14 | {% endset %} 15 | 16 | {% set html %} 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | {% endset %} 27 | 28 | 29 | 72 | 73 | 74 | {{ demo( label, html, script, style ) }} 75 | -------------------------------------------------------------------------------- /doc_src/pages/examples/contacts.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Email Contacts Example 3 | nav_title: Email Contacts 4 | tags: demo 5 | --- 6 | 7 | 8 | 9 | {% from "demo.njk" import demo %} 10 | 11 | {% set label %} 12 | 13 |

An example showing how you might go about creating contact selector like those used in Email apps.

14 |

Search by email address with "email:gmail.com" or last name with "last:tesla"

15 | {% endset %} 16 | 17 | {% set html %} 18 | 19 | {% endset %} 20 | 21 | 92 | 93 | 114 | 115 | {{ demo( label, html, script, style) }} 116 | -------------------------------------------------------------------------------- /doc_src/pages/examples/create-filter.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Item Creation Examples 3 | nav_title: Item Creation 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | 10 | 11 | {% set label %} 12 | 15 | {% endset %} 16 | 17 | {% set html %} 18 | 19 |
20 | 21 | 22 |
23 | {% endset %} 24 | 25 | 33 | 34 | {{ demo( label, html, script, style) }} 35 | 36 | {% set label %} 37 | 40 | {% endset %} 41 | 42 | {% set html %} 43 | 44 |
45 | 46 | 47 |
48 | {% endset %} 49 | 50 | 58 | 59 | {{ demo( label, html, script, style) }} 60 | 61 | 62 | {% set label %} 63 | 66 | {% endset %} 67 | 68 | {% set html %} 69 | 70 | {% endset %} 71 | 72 | 83 | 84 | {{ demo( label, html, script, style) }} 85 | -------------------------------------------------------------------------------- /doc_src/pages/examples/customization.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Customizing 3 | nav_title: Customizing 4 | tags: demo 5 | --- 6 | 7 |

8 | Nearly every piece of Tom Select is customizable. 9 | Render templates allow you to customize the HTML of rendered options, items, dropdown menu, optgroups and more. 10 |

11 | 12 | {% from "demo.njk" import demo %} 13 | 14 | {% set label %} 15 | 18 |

19 | This example provides a simple demonstration of how to override the default templates for options and items along with proper use of the escape() method. 20 |

21 | {% endset %} 22 | 23 | {% set html %} 24 | 25 | {% endset %} 26 | 27 | 39 | 40 | 41 | 66 | 67 | 68 | {{ demo( label, html, script, style) }} 69 | 70 | 71 | 72 | 73 | 74 | {% set label %} 75 | 78 |

79 | There are a number of ways to customize the JavaScript functionality. 80 | Plugins are a great example but sometimes you just want to add some functionality to items or options. 81 | The example below shows how to add a clickable button within an option but the same concept can be applied to items. 82 |

83 | {% endset %} 84 | 85 | {% set html %} 86 | 96 | {% endset %} 97 | 98 | 99 | 130 | 131 | 132 | {{ demo( label, html, script, style) }} 133 | -------------------------------------------------------------------------------- /doc_src/pages/examples/events.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Event Examples 3 | nav_title: Events 4 | tags: demo 5 | --- 6 | 7 | {% from "demo.njk" import demo %} 8 | 9 | {% set label %} 10 | 13 |

Check out the console for more details about each event.

14 | {% endset %} 15 | 16 | {% set html %} 17 | 71 |

Event Log

72 |

 73 | {% endset %}
 74 | 
 75 | 
 98 | 
 99 | {{ demo( label, html, script ) }}
100 | 
101 | 
102 | 
103 | {% set label %}
104 | 
107 | {% endset %}
108 | 
109 | {% set html %}
110 | 
111 | {% endset %}
112 | 
113 | {% set script %}
114 | new TomSelect("#input-tags",{
115 | 	delimiter: ",",
116 | 	persist: false,
117 | 	onDelete: function(values) {
118 | 		return confirm(values.length > 1 ? "Are you sure you want to remove these " + values.length + " items?" : "Are you sure you want to remove " + values[0] + "?");
119 | 	}
120 | });
121 | {% endset %}
122 | 
123 | {{ demo( label, html, script) }}
124 | 


--------------------------------------------------------------------------------
/doc_src/pages/examples/i18n.njk:
--------------------------------------------------------------------------------
  1 | ---
  2 | title: Internationalization
  3 | nav_title: Internationalization
  4 | tags: demo
  5 | ---
  6 | 
  7 | 
  8 | {% from "demo.njk" import demo %}
  9 | 
 10 | 
 11 | {% set label %}
 12 | 
 15 | 

16 | There are only a couple of default messages built into Tom Select. 17 | To customize these messages, and internationalize (i18n) your Tom Select instances, use the render template settings. 18 |

19 | {% endset %} 20 | 21 | {% set html %} 22 | 32 | {% endset %} 33 | 34 | 35 | 51 | 52 | 53 | {{ demo( label, html, script, style) }} 54 | 55 | 56 | {% set label %} 57 | 60 |

61 | Tom Select will work on RTL websites if the dir attribute is set for the context of your Tom Select instance. 62 |

63 | {% endset %} 64 | 65 | {% set html %} 66 |
67 | 68 |
69 | {% endset %} 70 | 71 | 72 | 80 | 81 | {{ demo( label, html, script) }} 82 | 83 | 84 | {% set label %} 85 | 88 | {% endset %} 89 | 90 | {% set html %} 91 | 98 | {% endset %} 99 | 100 | 101 | 106 | 107 | {{ demo( label, html, script) }} 108 | -------------------------------------------------------------------------------- /doc_src/pages/examples/lock.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Locking Example 3 | nav_title: Locking 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | 10 | 11 | {% set label %} 12 | 15 |

Controls can be locked to prevent user interaction.

16 | {% endset %} 17 | 18 | {% set html %} 19 | 24 | {% endset %} 25 | 26 | 31 | 32 | {{ demo( label, html, script) }} 33 | 34 | 35 | {% set label %} 36 | 39 | {% endset %} 40 | 41 | {% set html %} 42 | 47 | {% endset %} 48 | 49 | 54 | 55 | {{ demo( label, html, script) }} 56 | -------------------------------------------------------------------------------- /doc_src/pages/examples/options.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Options Array Examples 3 | nav_title: Options Array 4 | tags: demo 5 | --- 6 | 7 | {% from "demo.njk" import demo %} 8 | 9 | 10 | {% set label %} 11 | 14 |

The options are created from an array in JavaScript.

15 | {% endset %} 16 | 17 | {% set html %} 18 | 19 | {% endset %} 20 | 21 | 22 | 39 | 40 | {{ demo( label, html, script) }} 41 | 42 | 43 | 44 | {% set label %} 45 | 46 |

Images can be added to option and item elements with custom render templates and data-* attributes

47 | {% endset %} 48 | 49 | {% set html %} 50 | 55 | {% endset %} 56 | 57 | 58 | 72 | 73 | {{ demo( label, html, script) }} 74 | -------------------------------------------------------------------------------- /doc_src/pages/examples/performance.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Large Dataset Performance 3 | nav_title: Large Dataset 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | 10 | 11 | {% set label %} 12 | 15 |

This shows how it performs with 15,000 items.

{% endset %} 16 | 17 | {% set html %} 18 | 19 | {% endset %} 20 | 21 | 48 | 49 | {{ demo( label, html, script) }} 50 | -------------------------------------------------------------------------------- /doc_src/pages/examples/plugins.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Plugin Examples 3 | nav_title: Plugins 4 | layout: layout_redirect.njk 5 | destination: "/plugins/" 6 | exclude: true 7 | --- 8 | -------------------------------------------------------------------------------- /doc_src/pages/examples/required.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Required Input Example 3 | nav_title: Required Input 4 | layout: layout_redirect.njk 5 | destination: "/examples/validation/" 6 | exclude: true 7 | --- 8 | -------------------------------------------------------------------------------- /doc_src/pages/examples/rtl.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: RTL 3 | nav_title: RTL 4 | layout: layout_redirect.njk 5 | destination: "/examples/i18n/" 6 | exclude: true 7 | --- 8 | -------------------------------------------------------------------------------- /doc_src/pages/pages.json: -------------------------------------------------------------------------------- 1 | { "layout": "layout_default" } 2 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/caret-position.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Caret Position 3 | nav_title: Caret Position 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | {% from "macro_config.njk" import config_table %} 10 | 11 |

12 | Order matters sometimes. Use the and arrow keys to move the caret between items. 13 |

14 | 15 | 16 | {% set label %} 17 | 20 | {% endset %} 21 | 22 | {% set html %} 23 | 33 | {% endset %} 34 | 35 | 42 | 43 | {{ demo( label, html, script) }} 44 | 45 |

Plugin Configuration

46 | 47 |

No additional configuration settings for this plugin

48 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/change-listener.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Change Listener 3 | nav_title: Change Listener 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | 10 | 11 | 12 | {% set label %} 13 | 14 |

Listen for 'change' events on the original input/select and update Tom Select accordingly

15 | {% endset %} 16 | 17 | {% set html %} 18 | 19 | 20 | {% endset %} 21 | 22 | 40 | 41 | {{ demo( label, html, script ) }} 42 | 43 | 44 | 45 |

Plugin Configuration

46 |

No additional configuration settings for this plugin

47 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/checkbox-options.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Checkbox Options 3 | nav_title: Checkbox Options 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | 10 | 11 | {% set label %} 12 | 15 | {% endset %} 16 | 17 | {% set html %} 18 | 28 | {% endset %} 29 | 30 | 37 | 38 | {{ demo( label, html, script) }} 39 | 40 | 41 |

Plugin Configuration

42 |

No additional configuration settings for this plugin

43 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/clear-button.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Clear Button 3 | nav_title: Clear Button 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | {% from "macro_config.njk" import config_table %} 10 | 11 | 12 | {% set label %} 13 | 16 | {% endset %} 17 | 18 | 19 | {% set html %} 20 | 21 | {% endset %} 22 | 23 | 36 | 37 | 38 | {{ demo( label, html, script) }} 39 | 40 | 41 | {% set label %} 42 | 45 | {% endset %} 46 | 47 | 48 | {% set html %} 49 | 54 | {% endset %} 55 | 56 | 66 | 67 | 68 | {{ demo( label, html, script) }} 69 | 70 | 71 | {% set label %} 72 | 75 | {% endset %} 76 | 77 | 78 | {% set html %} 79 | 84 | {% endset %} 85 | 86 | 97 | 98 | 99 | {{ demo( label, html, script) }} 100 | 101 | 102 |

Plugin Configuration

103 | 104 | {{ config_table([ 105 | { 106 | name:'html', 107 | desc:'

A callback that returns an html string used to create the button

', 108 | type:'callback', 109 | default:'function(data){\n return `
×
`;\n}', 110 | highlight:true 111 | }, 112 | {name:'title',desc:'

The value of the title attribute on the close button

',type:'string',default:'Clear All'}, 113 | {name:'className',desc:'

The CSS class name of the close button

',type:'string',default:'clear-button'} 114 | ]) 115 | }} 116 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/drag-drop.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Drag 'n Drop 3 | nav_title: Drag 'n Drop 4 | tags: demo 5 | script: 6 | 7 | --- 8 | 9 | 10 | {% from "demo.njk" import demo %} 11 | 12 | 13 | 14 | 15 | {% set label %} 16 | 19 |

Drag 'n drop selected items. Requires jQuery + jQuery UI

20 | {% endset %} 21 | 22 | {% set html %} 23 | 24 | {% endset %} 25 | 26 | 35 | 36 | {{ demo( label, html, script,'',true) }} 37 | 38 | 39 |

Plugin Configuration

40 |

No additional configuration settings for this plugin

41 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/dropdown-header.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dropdown Header 3 | nav_title: Dropdown Header 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | {% from "macro_config.njk" import config_table %} 10 | 11 | 12 | 13 | {% set label %} 14 | 17 | {% endset %} 18 | 19 | {% set html %} 20 | 34 | {% endset %} 35 | 36 | 49 | 50 | {{ demo( label, html, script) }} 51 | 52 | 53 |

Plugin Configuration

54 | 55 | {{ config_table([ 56 | { name:'html', 57 | desc:'

An html string used to generate the header

', 58 | type:'callback', 59 | default:'function(data){\n return `
\n
\n ${data.title}\n ×\n
\n
`;\n}', 60 | highlight:true 61 | }, 62 | {name:'title',desc:'

The text of the header

',type:'string',default:'Untitled'}, 63 | {name:'headerClass',desc:'

The CSS class name of the header

',type:'string',default:'dropdown-header'}, 64 | {name:'titleRowClass',desc:'

The CSS class name of the dropdown header title

',type:'string',default:'dropdown-header-title'}, 65 | {name:'labelClass',desc:'

The CSS class name of the dropdown header label row

',type:'string',default:'dropdown-header-label'}, 66 | {name:'closeClass',desc:'

The CSS class name of the dropdown header close button

',type:'string',default:'dropdown-header-close'} 67 | ]) 68 | }} 69 | 70 | 71 | {# 72 | 73 | 74 | #} 75 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/dropdown-input.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dropdown Input 3 | nav_title: Dropdown Input 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | 10 | 11 | {% set label %} 12 | 15 | {% endset %} 16 | 17 | {% set html %} 18 | 28 | {% endset %} 29 | 30 | 37 | 38 | {{ demo( label, html, script) }} 39 | 40 | 41 |

Plugin Configuration

42 |

No additional configuration settings for this plugin

43 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/input-autogrow.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Input Autogrow 3 | nav_title: Input Autogrow 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | 10 | 11 | 12 | 13 | {% set label %} 14 | 17 |

18 | The input_autogrow plugin will increase the width of the input as users type. 19 |

20 | {% endset %} 21 | 22 | {% set html %} 23 |
24 | 34 |
35 | {% endset %} 36 | 37 | 44 | 45 | {{ demo( label, html, script) }} 46 | 47 | 48 |

Plugin Configuration

49 |

No additional configuration settings for this plugin

50 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/no-active-items.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: No Active Items 3 | nav_title: No Active Items 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | 10 | 11 | {% set label %} 12 | 15 |

16 | Prevents users from selecting items (eg selected options). 17 | Normally, users can active items by either clicking on them, shift+clicking, shift+left-arrow. 18 | This plugin disables that functionality. 19 |

20 | {% endset %} 21 | 22 | 23 | {% set html %} 24 | 25 | {% endset %} 26 | 27 | 36 | 37 | 38 | {{ demo( label, html, script) }} 39 | 40 | 41 |

Plugin Configuration

42 |

No additional configuration settings for this plugin

43 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/no-backspace-delete.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: No Backspace Delete 3 | nav_title: No Backspace Delete 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | 10 | 11 | {% set label %} 12 | 15 | {% endset %} 16 | 17 | {% set html %} 18 |
19 | 20 | 21 |
22 | {% endset %} 23 | 24 | 35 | 36 | 43 | 44 | {{ demo( label, html, script, style) }} 45 | 46 |

Plugin Configuration

47 |

No additional configuration settings for this plugin

48 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/optgroup-columns.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Option Group Columns 3 | nav_title: Option Group Columns 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | 10 | 11 | 12 | 13 | {% set label %} 14 | 17 |

18 | The opgroup_columns plugin uses CSS flexbox layouts to display optgroups in columns and adds keyboard shortcuts for navigating between columnns with and keys. 19 |

20 | {% endset %} 21 | 22 | {% set html %} 23 | 24 | {% endset %} 25 | 26 | 68 | 69 | {{ demo( label, html, script) }} 70 | 71 |

Plugin Configuration

72 |

No additional configuration settings for this plugin

73 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/plugins.json: -------------------------------------------------------------------------------- 1 | { "layout": "layout_plugin" } 2 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/remove-button.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Remove Button 3 | nav_title: Remove Button 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | {% from "macro_config.njk" import config_table %} 10 | 11 | 12 | 13 | 14 | 15 | {% set label %} 16 | 19 | {% endset %} 20 | 21 | {% set html %} 22 | 23 | {% endset %} 24 | 25 | 41 | 42 | {{ demo( label, html, script) }} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {% set label %} 52 | 55 |

56 | For single selects, you may prefer the Clear Button plugin 57 |

58 | {% endset %} 59 | 60 | {% set html %} 61 | 65 | {% endset %} 66 | 67 | 77 | 78 | {{ demo( label, html, script) }} 79 | 80 | 81 | 82 |

Plugin Configuration

83 | 84 | {{ config_table([ 85 | {name:'label',desc:'

The text that will be displayed in each button

',type:'string',default:'×'}, 86 | {name:'title',desc:'

The value of the title attribute on each button

',type:'string',default:'Remove'}, 87 | {name:'className',desc:'

The CSS class name of each button

',type:'string',default:'remove'} 88 | ]) 89 | }} 90 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/restore-on-backspace.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Restore on Backspace 3 | nav_title: Restore on Backspace 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | {% from "macro_config.njk" import config_table %} 10 | 11 | 12 | 13 | {% set label %} 14 | 17 | {% endset %} 18 | 19 | {% set html %} 20 | 21 | {% endset %} 22 | 23 | 32 | 33 | {{ demo( label, html, script) }} 34 | 35 | 36 |

Plugin Configuration

37 | 38 | {{ config_table([ 39 | { 40 | name:'html', 41 | desc:'

A function taking option data as an argument and returning a text string that will be assigned to the control input value

', 42 | type:'callback', 43 | default:'function(option){\n return option[self.settings.labelField];\n}', 44 | highlight:true 45 | } 46 | ]) 47 | }} 48 | -------------------------------------------------------------------------------- /doc_src/pages/plugins/virtual_scroll.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Virtual Scroll 3 | nav_title: Virtual Scroll 4 | tags: demo 5 | --- 6 | 7 | 8 | {% from "demo.njk" import demo %} 9 | {% from "macro_config.njk" import config_table %} 10 | 11 | 12 | 13 | {% set label %} 14 | 17 | {% endset %} 18 | 19 | {% set html %} 20 | 21 | {% endset %} 22 | 23 | 71 | 72 | {{ demo( label, html, script) }} 73 | 74 | 75 |

Related Tom Select Settings

76 | 77 | {{ config_table([ 78 | { 79 | name:'firstUrl', 80 | desc:'(Required) 81 | The firstUrl() method works along with getUrl() and setNextUrl() to supply your load() method with pagination friendly urls. 82 | ', 87 | type:'callback', 88 | default:'null' 89 | }, 90 | { 91 | name:'shouldLoadMore', 92 | desc:'The shouldLoadMore() method determines if additinal results should be loaded based on the dropdown scroll position.', 93 | type:'callback', 94 | default:'callback' 95 | }, 96 | { 97 | name:'maxOptions', 98 | desc:'The max number of options to display in the dropdown.', 99 | type:'int', 100 | default:'50' 101 | }, 102 | { 103 | name:'render.loading_more', 104 | desc:'Renders a message at the bottom of the dropdown to communicate to users that more results are being loaded', 105 | type:'callback', 106 | default:'function(data,escape){\n return `
Loading more results ...
`;\n}', 107 | highlight:true 108 | }, 109 | { 110 | name:'render.no_more_results', 111 | desc:'Renders a message at the bottom of the dropdown for communicating that users have reached the end of the results', 112 | type:'callback', 113 | default:'function(data,escape){\n return `
No more results
`;\n}', 114 | highlight:true 115 | } 116 | 117 | ]) 118 | }} 119 | 120 | 121 |

Plugin Configuration

122 | 123 |

No additional configuration settings for this plugin

124 | -------------------------------------------------------------------------------- /doc_src/pages/robots.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /robots.txt 3 | eleventyExcludeFromCollections: true 4 | layout: 5 | --- 6 | sitemap: /sitemap.xml 7 | 8 | User-agent: * 9 | Disallow: 10 | -------------------------------------------------------------------------------- /doc_src/pages/sitemap.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /sitemap.xml 3 | eleventyExcludeFromCollections: true 4 | layout: 5 | --- 6 | 7 | 8 | {% for page in collections.all %} 9 | 10 | {{ site.url }}{{ page.url | url }} 11 | {{ page.date.toISOString() }} 12 | 13 | {% endfor %} 14 | 15 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=$1 4 | 5 | # This is a general-purpose function to ask Yes/No questions in Bash, either 6 | # with or without a default answer. It keeps repeating the question until it 7 | # gets a valid answer. 8 | # https://gist.github.com/davejamesmiller/1965569 9 | ask() { 10 | local prompt default reply 11 | 12 | if [ "${2:-}" = "Y" ]; then 13 | prompt="Y/n" 14 | default=Y 15 | elif [ "${2:-}" = "N" ]; then 16 | prompt="y/N" 17 | default=N 18 | else 19 | prompt="y/n" 20 | default= 21 | fi 22 | 23 | while true; do 24 | 25 | # Ask the question (not using "read -p" as it uses stderr not stdout) 26 | echo -n "$1 [$prompt] " 27 | 28 | # Read the answer (use /dev/tty in case stdin is redirected from somewhere else) 29 | read reply 3 | * Highlights arbitrary terms in a node. 4 | * 5 | * - Modified by Marshal 2011-6-24 (added regex) 6 | * - Modified by Brian Reavis 2012-8-27 (cleanup) 7 | */ 8 | 9 | import {replaceNode} from '../vanilla'; 10 | 11 | 12 | export const highlight = (element:HTMLElement, regex:string|RegExp) => { 13 | 14 | if( regex === null ) return; 15 | 16 | // convet string to regex 17 | if( typeof regex === 'string' ){ 18 | 19 | if( !regex.length ) return; 20 | regex = new RegExp(regex, 'i'); 21 | } 22 | 23 | 24 | // Wrap matching part of text node with highlighting , e.g. 25 | // Soccer -> Soccer for regex = /soc/i 26 | const highlightText = ( node:Text ):number => { 27 | 28 | var match = node.data.match(regex); 29 | if( match && node.data.length > 0 ){ 30 | var spannode = document.createElement('span'); 31 | spannode.className = 'highlight'; 32 | var middlebit = node.splitText(match.index as number); 33 | 34 | middlebit.splitText(match[0]!.length); 35 | var middleclone = middlebit.cloneNode(true); 36 | 37 | spannode.appendChild(middleclone); 38 | replaceNode(middlebit, spannode); 39 | return 1; 40 | } 41 | 42 | return 0; 43 | }; 44 | 45 | // Recurse element node, looking for child text nodes to highlight, unless element 46 | // is childless, 8 | 9 | 10 | 11 | 12 | 13 |
14 | 24 |
25 | 26 | 27 | 41 | 42 | -------------------------------------------------------------------------------- /test/html/tab.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 | 26 | 36 |
37 |
38 | 39 |
40 | 41 |
42 |
43 | 44 | 45 | 46 | 57 | 58 | -------------------------------------------------------------------------------- /test/html/virtual-scroll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 62 | 63 | -------------------------------------------------------------------------------- /test/support/async.js: -------------------------------------------------------------------------------- 1 | 2 | function mouseEvent(el,evt='click') { 3 | const event = new MouseEvent(evt, { 4 | view: window, 5 | bubbles: true, 6 | cancelable: true, 7 | composed: true 8 | }); 9 | const cancelled = !el.dispatchEvent(event); 10 | return cancelled; 11 | } 12 | 13 | 14 | async function asyncClick(el){ 15 | 16 | if( mouseEvent(el,'mousedown') ){ 17 | mouseEvent(el,'click'); 18 | } 19 | 20 | await waitFor(100); 21 | } 22 | 23 | async function asyncType(text){ 24 | return new Promise(resolve => { 25 | syn.type(text,document.activeElement,()=>{ 26 | resolve(); 27 | }); 28 | }); 29 | } 30 | 31 | 32 | async function waitFor(delay){ 33 | return new Promise(resolve => { 34 | setTimeout(resolve,delay); 35 | }); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /test/support/base.js: -------------------------------------------------------------------------------- 1 | window.expect = chai.expect; 2 | window.assert = chai.assert; 3 | window.has_focus = function(elem) { 4 | return !!(elem === document.activeElement); 5 | }; 6 | 7 | var current_test_label = document.createElement('h1'); 8 | current_test_label.setAttribute('style', 'white-space:nowrap;overflow:hidden'); 9 | document.body.appendChild(current_test_label); 10 | 11 | 12 | var sandbox = document.createElement('div'); 13 | sandbox.setAttribute('role','main'); 14 | document.body.appendChild(sandbox); 15 | 16 | var preventHover = document.createElement('div'); 17 | preventHover.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;z-index:10000'; 18 | document.body.appendChild(preventHover); 19 | 20 | 21 | var IS_MAC = /Mac/.test(navigator.userAgent); 22 | var shortcut_key = IS_MAC ? 'meta' : 'ctrl'; 23 | var test_number = 0; 24 | 25 | 26 | var teardownLast = function(){ 27 | if( window.test_last ){ 28 | if( window.test_last.instance ){ 29 | window.test_last.instance.destroy(); 30 | delete window.test_last.instance; 31 | } 32 | sandbox.innerHTML = ''; 33 | window.test_last = null; 34 | } 35 | } 36 | 37 | var test_html = { 38 | AB_Multi : '', 39 | AB_Single : '', 40 | AB_Single_Long : '', 41 | } 42 | 43 | Array.prototype.foo = function(){ 44 | return true; 45 | } 46 | 47 | window.setup_test = function(html, options, callback) { 48 | var instance, select; 49 | teardownLast(); 50 | 51 | if( html in test_html ){ 52 | html = test_html[html]; 53 | } 54 | 55 | if( typeof html == 'string' ){ 56 | sandbox.innerHTML = html; 57 | }else{ 58 | sandbox.append(html); 59 | } 60 | 61 | 62 | select = sandbox.querySelector('.setup-here'); 63 | if( !select ){ 64 | select = sandbox.firstChild; 65 | } 66 | 67 | if( select.nodeName == 'SELECT' || select.nodeName == 'INPUT' ){ 68 | instance = tomSelect(select,options); 69 | } 70 | 71 | var test = window.test_last = { 72 | html: sandbox.firstChild, 73 | select: select, 74 | callback: callback, 75 | instance: instance 76 | }; 77 | 78 | return test; 79 | }; 80 | 81 | /** 82 | * Create a test with two options 83 | * 84 | */ 85 | window.ABTestSingle = function(options){ 86 | return setup_test('ABTestSingle', options); 87 | }; 88 | 89 | 90 | after(function() { 91 | window.teardownLast(); 92 | }); 93 | 94 | 95 | var it_n = function(label,orig_func){ 96 | var new_func; 97 | 98 | label = (test_number++) + ' - ' + label 99 | 100 | if( orig_func.length > 0 ){ 101 | new_func = function(done){ 102 | current_test_label.textContent = label; 103 | return orig_func.call(this,done); 104 | }; 105 | }else{ 106 | 107 | var func = orig_func.toString(); 108 | if( func.match(/(\s|syn\.)(type|click)\(/) ){ 109 | throw 'test should be async or use done():'+func; 110 | } 111 | new_func = function(){ 112 | current_test_label.textContent = label; 113 | return orig_func.call(this); 114 | }; 115 | } 116 | 117 | it.call( this, label, new_func ); 118 | } 119 | 120 | 121 | var click = function(el, cb) { 122 | syn.click(el).delay(100, cb); 123 | }; 124 | 125 | function isVisible(el){ 126 | return (el.offsetParent !== null) 127 | } 128 | -------------------------------------------------------------------------------- /test/tests/a11y.js: -------------------------------------------------------------------------------- 1 | describe("A11Y Compliance", function () { 2 | const html = `
3 |
`; 10 | 11 | it.skip("setup", (done) => { 12 | const test = setup_test(html); 13 | 14 | aChecker.getCompliance(test.instance.wrapper, "setup", (results) => { 15 | const returnCode = aChecker.assertCompliance(results); 16 | 17 | assert.equal( 18 | returnCode, 19 | 0, 20 | "A11Y Scan failed." + JSON.stringify(results) 21 | ); 22 | 23 | done(); 24 | }); 25 | }).timeout(5000); 26 | 27 | it.skip("isOpen", (done) => { 28 | const test = setup_test(html); 29 | 30 | click(test.instance.control, () => { 31 | assert.equal(test.instance.isOpen, true); 32 | 33 | aChecker.getCompliance( 34 | test.instance.wrapper, 35 | "isOpen", 36 | (results) => { 37 | const returnCode = aChecker.assertCompliance(results); 38 | 39 | assert.equal( 40 | returnCode, 41 | 0, 42 | "A11Y Scan failed." + JSON.stringify(results) 43 | ); 44 | 45 | done(); 46 | } 47 | ); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/tests/config-control-input.js: -------------------------------------------------------------------------------- 1 | 2 | describe('Control Input', function() { 3 | 4 | describe('controlInput = null', function() { 5 | it_n('should initialize without control input',async function(){ 6 | var test = setup_test('AB_Multi', { 7 | controlInput: null, 8 | }); 9 | 10 | assert.isNotOk( test.html.querySelector('input'), 'should initialize without control input'); 11 | }); 12 | 13 | it_n('should select option with [enter] keypress (single)', async () => { 14 | 15 | var test = setup_test('AB_Single',{ 16 | controlInput: null, 17 | }); 18 | 19 | await asyncClick( test.instance.control ); 20 | assert.isTrue( test.instance.isOpen ); 21 | assert.equal(test.instance.activeOption.dataset.value,'a'); 22 | 23 | await asyncType('[enter]'); 24 | assert.isFalse( test.instance.isOpen ); 25 | assert.equal( test.instance.items.length, 1); 26 | assert.equal( test.instance.items[0], 'a'); 27 | 28 | await asyncType('[down]'); 29 | assert.isTrue( test.instance.isOpen ); 30 | 31 | await asyncType('[down][enter]'); 32 | assert.equal( test.instance.items.length, 1); 33 | assert.equal( test.instance.items[0], 'b'); 34 | 35 | }); 36 | 37 | it_n('only open after arrow down when openOnFocus=false', async () => { 38 | 39 | var test = setup_test('AB_Single',{ 40 | controlInput: null, 41 | openOnFocus: false, 42 | }); 43 | 44 | await asyncClick(test.instance.control); 45 | assert.isFalse(test.instance.isOpen); 46 | 47 | await asyncType('[down]'); 48 | assert.isTrue(test.instance.isOpen); 49 | }); 50 | 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /test/tests/config-duplicates.js: -------------------------------------------------------------------------------- 1 | 2 | describe('duplicates', function() { 3 | 4 | it_n('should add and remove duplicates',async function(){ 5 | var test = setup_test('AB_Multi',{ 6 | duplicates: true, 7 | }); 8 | 9 | // add 10 | assert.equal(0,test.instance.items.length,0,'should start empty'); 11 | test.instance.addItem('a'); 12 | assert.equal(test.instance.items.length,1,'should add one'); 13 | assert.equal(test.instance.input.querySelectorAll('option[value="a"]').length, 1, 'should have 1 option w/ value=a in original select'); 14 | 15 | test.instance.addItem('a'); 16 | assert.equal(test.instance.items.length,2,'should add second'); 17 | assert.equal(test.instance.input.querySelectorAll('option[value="a"]').length, 2,'should have 3 options w/ value=a in original select'); 18 | 19 | test.instance.addItem('a'); 20 | assert.equal(test.instance.items.length,3,'should have all three'); 21 | assert.equal(test.instance.input.querySelectorAll('option[value="a"]').length, 3, 'should have 3 option w/ value=a in original select'); 22 | 23 | // remove items in order 24 | const items = test.instance.controlChildren(); 25 | await asyncClick(test.instance.control_input); 26 | 27 | while( items.length ){ 28 | items.pop(); 29 | await asyncType('\b'); 30 | const items_after = test.instance.controlChildren(); 31 | assert.deepEqual( items, items_after); 32 | } 33 | }); 34 | 35 | it_n('should initialize with duplicates (input)',async ()=>{ 36 | var test = setup_test('',{duplicates:true}); 37 | assert.equal(test.instance.items.length,2); 38 | }); 39 | 40 | it_n('should initialize with duplicates (select)',async ()=>{ 41 | var test = setup_test('',{duplicates:true}); 42 | assert.equal(test.instance.items.length,2); 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /test/tests/config-hidePlaceholder.js: -------------------------------------------------------------------------------- 1 | 2 | describe('hidePlaceholder', function() { 3 | 4 | it_n('should hide placeholder when option selected', async () => { 5 | 6 | var test = setup_test('AB_Multi',{ 7 | hidePlaceholder: true, 8 | placeholder: 'test-placeholder', 9 | }); 10 | 11 | await asyncClick(test.instance.control); 12 | assert.isTrue( test.instance.isOpen ); 13 | assert.equal( test.instance.control_input.placeholder, 'test-placeholder','placeholder should match setting'); 14 | 15 | var option = test.instance.dropdown_content.querySelector('[data-value="a"]'); 16 | 17 | await asyncClick(option); 18 | assert.equal( test.instance.items.length, 1); 19 | assert.equal( test.instance.control_input.placeholder, '', 'placeholder should be empty'); 20 | 21 | await asyncType( '\b'); 22 | assert.equal( test.instance.items.length, 0); 23 | assert.equal( test.instance.control_input.placeholder, 'test-placeholder', 'placeholder should match setting'); 24 | 25 | }); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /test/tests/config-load.js: -------------------------------------------------------------------------------- 1 | 2 | describe('load', function() { 3 | 4 | it_n('should start loading results if preload:"focus"', async () => { 5 | var calls_focus = 0; 6 | var calls_load = 0; 7 | 8 | var test = setup_test('AB_Single',{ 9 | preload: 'focus', 10 | load: function(query, load_cb) { 11 | calls_load++; 12 | assert.equal(query, ''); 13 | setTimeout(function() { 14 | load_cb([{value: 'c', text: 'C'}]); 15 | },100); 16 | } 17 | }); 18 | 19 | test.instance.on('focus', function() { 20 | calls_focus++; 21 | }); 22 | 23 | await asyncClick(test.instance.control); 24 | assert.isOk(test.instance.dropdown.querySelector('.spinner')); 25 | 26 | await waitFor(300); 27 | assert.equal(calls_focus, 1); 28 | assert.equal(calls_load, 1); 29 | 30 | await asyncClick(document.body); 31 | await asyncClick(test.instance.control); 32 | await waitFor(300); 33 | assert.equal(calls_focus, 2); 34 | assert.equal(calls_load, 1); 35 | 36 | 37 | }); 38 | 39 | 40 | it_n('should start loading if preload:true', function(done) { 41 | 42 | setup_test('AB_Single',{ 43 | preload: true, 44 | load: function(query, load_cb) { 45 | assert.equal(query, ''); 46 | load_cb([{value: 'c', text: 'C'}]); 47 | done(); 48 | } 49 | }); 50 | }); 51 | 52 | it_n('should not show no_results message while/after loading', function(done) { 53 | 54 | var test = setup_test('',{ 124 | load: function(query, load_cb) { 125 | if ( query.length < 3 ) return load_cb(); 126 | }, 127 | render: { 128 | no_results: (data,escape) => { 129 | if( data.input.length < 3) return; 130 | return '
No results found
'; 131 | }, 132 | loading: (data,escape) => { 133 | if( data.input.length < 3 ) return; 134 | return '
'; 135 | }, 136 | } 137 | }); 138 | 139 | syn.type('a', test.instance.control_input,function(){ 140 | setTimeout(function(){ 141 | assert.equal(test.instance.isOpen, false); 142 | done(); 143 | },100); 144 | }); 145 | 146 | }); 147 | 148 | 149 | }); 150 | -------------------------------------------------------------------------------- /test/tests/config.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | describe('Configuration settings', function() { 4 | 5 | describe('hideSelected', function() { 6 | 7 | it_n('option should still be shown when selected', function(done) { 8 | 9 | var test = setup_test('AB_Multi',{hideSelected:false,closeAfterSelect:true}); 10 | 11 | click(test.instance.control, function() { 12 | 13 | var options = test.instance.dropdown.querySelectorAll('.option'); 14 | expect(options.length).to.be.equal(3); 15 | expect(test.instance.items.length).to.be.equal(0); 16 | 17 | var option_a = test.instance.dropdown_content.querySelector('[data-value="a"]'); 18 | 19 | click( option_a, function() { 20 | options = test.instance.dropdown.querySelectorAll('.option'); 21 | expect(options.length).to.be.equal(3); 22 | expect(test.instance.items.length).to.be.equal(1); 23 | 24 | assert.isFalse( test.instance.isOpen, 'should be closed after selected'); 25 | assert.equal(option_a, test.instance.activeOption, 'active option should be set after closing'); 26 | 27 | done(); 28 | }); 29 | }); 30 | }); 31 | 32 | 33 | it_n('option should not be shown when selected', function(done) { 34 | 35 | var test = setup_test('',{hideSelected:true,options:[{value:'a'},{value:'b'},{value:'c'}]}); 36 | 37 | click(test.instance.control, function() { 38 | 39 | var options = test.instance.dropdown.querySelectorAll('.option'); 40 | expect(options.length).to.be.equal(3); 41 | expect(test.instance.items.length).to.be.equal(0); 42 | 43 | click( test.instance.dropdown_content.querySelector('[data-value="a"]'), function() { 44 | options = test.instance.dropdown.querySelectorAll('.option'); 45 | expect(options.length).to.be.equal(2); 46 | expect(test.instance.items.length).to.be.equal(1); 47 | 48 | done(); 49 | }); 50 | }); 51 | }); 52 | 53 | it_n('should allow duplicate options when hideSelected=false and duplicates=true',function(done){ 54 | var test = setup_test('AB_Multi',{hideSelected:false,duplicates:true}); 55 | 56 | click(test.instance.control, function() { 57 | 58 | var options = test.instance.dropdown.querySelectorAll('.option'); 59 | expect(options.length).to.be.equal(3); 60 | expect(test.instance.items.length).to.be.equal(0); 61 | 62 | click( test.instance.dropdown_content.querySelector('[data-value="a"]'), function() { 63 | 64 | click( test.instance.dropdown_content.querySelector('[data-value="a"]'), function() { 65 | options = test.instance.dropdown.querySelectorAll('.option'); 66 | expect(options.length).to.be.equal(3); 67 | expect(test.instance.items.length).to.be.equal(2); 68 | 69 | done(); 70 | }); 71 | }); 72 | }); 73 | }); 74 | 75 | }); 76 | 77 | describe('copyClassesToDropdown',function(){ 78 | 79 | it_n('class should be copied', function() { 80 | var test = setup_test('',{copyClassesToDropdown:true}); 81 | expect(test.instance.dropdown.classList.contains('classA')).to.be.true 82 | }); 83 | 84 | it_n('class should not be copied', function() { 85 | var test = setup_test('',{copyClassesToDropdown:false}); 86 | expect(test.instance.dropdown.classList.contains('classA')).to.be.false 87 | 88 | }); 89 | 90 | }); 91 | 92 | describe('onInitialize',function(){ 93 | 94 | it_n('onInitialize should be called', function(done) { 95 | setup_test('',{ 96 | onInitialize:function(){ 97 | expect(true).to.be.true 98 | done(); 99 | } 100 | }); 101 | }); 102 | 103 | }); 104 | 105 | it_n('allowEmptyOption', async () => { 106 | 107 | let test = setup_test(``, {allowEmptyOption:true}); 112 | 113 | assert.equal( Object.keys(test.instance.options).length, 3); 114 | assert.equal( test.instance.items.length, 0); 115 | 116 | await asyncClick(test.instance.control); 117 | assert.isTrue(test.instance.isOpen); 118 | var opt = test.instance.getOption(''); 119 | await asyncClick(opt); 120 | assert.equal( test.instance.items.length, 1); 121 | }); 122 | 123 | }); 124 | -------------------------------------------------------------------------------- /test/tests/esm-module.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | describe('ESM Module', function(d1){ 5 | 6 | this.timeout(10000); 7 | 8 | it_n('should initialize without exceptions', async () =>{ 9 | 10 | import('/base/build/esm/tom-select.complete.js').then(function(SelectModule){ 11 | var instance = new SelectModule.default('', { 5 | valueField: 'value', 6 | labelField: 'value', 7 | options: [ 8 | {value: 'a'}, 9 | {value: 'b'}, 10 | ], 11 | items: ['a'] 12 | }); 13 | 14 | var counter = 0; 15 | test.instance.on('change', function() { counter++; }); 16 | test.instance.addItem('b'); 17 | 18 | window.setTimeout(function() { 19 | expect(counter).to.be.equal(1); 20 | done(); 21 | }, 0); 22 | }); 23 | it_n('should be triggered once by removeItem()', function(done) { 24 | var test = setup_test('', { 45 | valueField: 'value', 46 | labelField: 'value', 47 | options: [ 48 | {value: 'a'}, 49 | {value: 'b'}, 50 | ], 51 | items: ['a','b'] 52 | }); 53 | 54 | var counter = 0; 55 | test.instance.on('change', function() { counter++; }); 56 | test.instance.clear(); 57 | 58 | window.setTimeout(function() { 59 | expect(counter).to.be.equal(1); 60 | done(); 61 | }, 0); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/tests/plugins/caret_position.js: -------------------------------------------------------------------------------- 1 | describe('plugin: caret_position', function() { 2 | 3 | it_n('should move caret when [left] or [right] pressed', function(done) { 4 | var test = setup_test('', { 5 | plugins:['caret_position'], 6 | create: true 7 | }); 8 | 9 | click(test.instance.control, function() { 10 | syn.type('[left][left]whatt', test.instance.control_input, function() { 11 | expect(test.instance.caretPos).to.be.equal(2); 12 | done(); 13 | }); 14 | }); 15 | }); 16 | 17 | it_n('should move caret left when [left] pressed, then should select item after control when ['+shortcut_key+'][right] pressed', function(done) { 18 | var test = setup_test('AB_Multi',{ 19 | plugins:['caret_position'], 20 | }); 21 | 22 | test.instance.addItem('a'); 23 | test.instance.addItem('b'); 24 | var itemb = test.instance.getItem('b'); 25 | var itema = test.instance.getItem('a'); 26 | 27 | click(test.instance.control, function() { 28 | 29 | syn.type('[left]['+shortcut_key+'][right]['+shortcut_key+'-up]', test.instance.control_input, function() { 30 | 31 | assert.equal( test.instance.activeItems.length , 1); 32 | assert.equal( test.instance.activeItems[0] , itemb); 33 | assert.equal( itemb.previousElementSibling, test.instance.control_input ); 34 | 35 | done(); 36 | }); 37 | }); 38 | 39 | }); 40 | 41 | it_n('should move caret before selected item when [left] pressed', function(done) { 42 | var test = setup_test('AB_Multi',{ 43 | plugins:['caret_position'], 44 | }); 45 | 46 | test.instance.addItem('a'); 47 | test.instance.addItem('b'); 48 | var itema = test.instance.getItem('a'); 49 | var itemb = test.instance.getItem('b'); 50 | 51 | click(test.instance.control, function() { 52 | 53 | test.instance.setActiveItem(itemb); 54 | 55 | expect( itemb.nextElementSibling ).to.be.equal( test.instance.control_input ); 56 | 57 | syn.type('[left]', test.instance.control_input, function() { 58 | expect( itemb.previousElementSibling ).to.be.equal( test.instance.control_input ); 59 | done(); 60 | }); 61 | }); 62 | 63 | }); 64 | 65 | 66 | it_n('should remove first item when left then backspace pressed', function(done) { 67 | 68 | var test = setup_test('AB_Multi',{ 69 | plugins:['caret_position'], 70 | }); 71 | 72 | test.instance.addItem('a'); 73 | test.instance.addItem('b'); 74 | assert.equal( test.instance.items.length, 2 ); 75 | 76 | click(test.instance.control, function() { 77 | syn.type('[left]\b', test.instance.control_input, function() { 78 | 79 | assert.equal( test.instance.items.length, 1 ); 80 | assert.equal( test.instance.items[0], 'b' ); 81 | done(); 82 | }); 83 | }); 84 | 85 | }); 86 | 87 | it_n('move after active item', async() => { 88 | 89 | var test = setup_test('AB_Multi',{ 90 | plugins:['caret_position'], 91 | }); 92 | 93 | test.instance.addItem('a'); 94 | test.instance.addItem('b'); 95 | assert.equal( test.instance.items.length, 2 ); 96 | 97 | var itemb = test.instance.getItem('b'); 98 | await asyncClick(itemb); 99 | 100 | assert.isTrue( itemb.classList.contains('last-active') ); 101 | 102 | assert.equal(test.instance.caretPos,2); 103 | await asyncType('[left]'); 104 | assert.equal(test.instance.caretPos,1); 105 | await asyncType('[left]'); 106 | assert.equal(test.instance.caretPos,0); 107 | 108 | }); 109 | 110 | }); 111 | -------------------------------------------------------------------------------- /test/tests/plugins/change_listener.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | describe('plugin: change_listener', function() { 5 | 6 | function changeInput(input,value = null ){ 7 | if( value ){ 8 | input.value = value; 9 | } 10 | var evt = document.createEvent('HTMLEvents'); 11 | evt.initEvent('change', false, true); 12 | input.dispatchEvent(evt); 13 | } 14 | 15 | it_n('should update when original input is changed', async ()=> { 16 | 17 | let test = setup_test('', {plugins: ['change_listener']}); 18 | 19 | assert.equal( Object.keys(test.instance.options).length, 1); 20 | 21 | var input = test.select; 22 | 23 | changeInput(input,'new value'); 24 | await waitFor(10); 25 | assert.equal( Object.keys(test.instance.options).length, 2); 26 | assert.equal( test.instance.items.length, 1,'should have one value = "new value"'); 27 | assert.equal( test.instance.items[0], 'new value'); 28 | assert.isFalse( test.instance.isFocused, 'should not focus'); 29 | 30 | test.instance.removeItem('new value'); 31 | await waitFor(10); 32 | assert.equal( test.instance.items.length, 0); 33 | 34 | changeInput(input,'another value'); 35 | await waitFor(10); 36 | assert.equal( test.instance.items[0], 'another value'); 37 | assert.equal( test.instance.items.length, 1,'should have one value = "another value"'); 38 | }); 39 | 40 | it_n('typing in input with delimiter = " "', async ()=>{ 41 | 42 | let test = setup_test('', {plugins: ['change_listener'],delimiter:' ',create:true}); 43 | 44 | await asyncClick(test.instance.control); 45 | assert.isTrue(test.instance.isFocused,'should be focused'); 46 | 47 | await asyncType('new[enter]'); 48 | await waitFor(10); 49 | 50 | assert.equal(test.instance.control_input, document.activeElement,'should maintain input focus'); 51 | assert.equal(test.instance.items.length, 2,'should have two items'); 52 | assert.equal(test.instance.items[0], 'original'); 53 | assert.equal(test.instance.items[1], 'new'); 54 | 55 | }); 56 | 57 | it_n('