├── .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) UI control. With autocomplete and native-feeling keyboard navigation, it's useful for tagging, contact lists, country selectors, and so on.", 4 | "type": "component", 5 | "homepage": "https://tom-select.js.org/", 6 | "license": "Apache-2.0", 7 | "extra": { 8 | "component": { 9 | "scripts": [ 10 | "dist/js/tom-select.complete.min.js" 11 | ], 12 | "styles": [ 13 | "dist/css/tom-select.default.min.css" 14 | ], 15 | "files": [ 16 | "dist/js/tom-select.complete.min.js", 17 | "dist/css/tom-select.default.min.css" 18 | ] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /doc_src/css/bootstrap.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v5.0.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors 4 | * Copyright 2011-2021 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | */ 7 | 8 | 9 | // saves ~40kb by removing lg, xl, xxl 10 | $grid-breakpoints: ( 11 | xs: 0, 12 | //sm: 576px, // 12kb 13 | md: 768px, 14 | //lg: 992px, 15 | //xl: 1200px, 16 | //xxl: 1400px 17 | ); 18 | 19 | // scss-docs-start import-stack 20 | // Configuration 21 | @import "../../node_modules/bootstrap5/scss/bootstrap-utilities"; 22 | 23 | // Layout & components 24 | @import "../../node_modules/bootstrap5/scss/root"; 25 | @import "../../node_modules/bootstrap5/scss/reboot"; 26 | @import "../../node_modules/bootstrap5/scss/type"; 27 | @import "../../node_modules/bootstrap5/scss/images"; 28 | @import "../../node_modules/bootstrap5/scss/containers"; 29 | @import "../../node_modules/bootstrap5/scss/grid"; 30 | @import "../../node_modules/bootstrap5/scss/tables"; 31 | @import "../../node_modules/bootstrap5/scss/forms"; 32 | @import "../../node_modules/bootstrap5/scss/buttons"; 33 | @import "../../node_modules/bootstrap5/scss/transitions"; 34 | @import "../../node_modules/bootstrap5/scss/dropdown"; 35 | //@import "../../node_modules/bootstrap5/scss/button-group"; 36 | @import "../../node_modules/bootstrap5/scss/nav"; 37 | @import "../../node_modules/bootstrap5/scss/navbar"; 38 | @import "../../node_modules/bootstrap5/scss/card"; 39 | //@import "../../node_modules/bootstrap5/scss/accordion"; 40 | //@import "../../node_modules/bootstrap5/scss/breadcrumb"; 41 | //@import "../../node_modules/bootstrap5/scss/pagination"; 42 | @import "../../node_modules/bootstrap5/scss/badge"; 43 | //@import "../../node_modules/bootstrap5/scss/alert"; 44 | //@import "../../node_modules/bootstrap5/scss/progress"; 45 | //@import "../../node_modules/bootstrap5/scss/list-group"; 46 | @import "../../node_modules/bootstrap5/scss/close"; 47 | //@import "../../node_modules/bootstrap5/scss/toasts"; 48 | //@import "../../node_modules/bootstrap5/scss/modal"; 49 | //@import "../../node_modules/bootstrap5/scss/tooltip"; 50 | //@import "../../node_modules/bootstrap5/scss/popover"; 51 | //@import "../../node_modules/bootstrap5/scss/carousel"; 52 | //@import "../../node_modules/bootstrap5/scss/spinners"; 53 | //@import "../../node_modules/bootstrap5/scss/offcanvas"; 54 | 55 | // Helpers 56 | @import "../../node_modules/bootstrap5/scss/helpers"; 57 | 58 | // Utilities 59 | @import "../../node_modules/bootstrap5/scss/utilities/api"; 60 | // scss-docs-end import-stack 61 | -------------------------------------------------------------------------------- /doc_src/css/variables.scss: -------------------------------------------------------------------------------- 1 | // colors 2 | $min-contrast-ratio: 1.5 !default; 3 | $text-muted-opacity: .7 !default; 4 | $text-muted-light-opacity: .4 !default; 5 | $text-muted-dark-opacity: .8 !default; 6 | 7 | $border-opacity: .16 !default; 8 | $border-dark-opacity: .24 !default; 9 | 10 | $light: #f4f6fa !default; 11 | $dark: #232e3c !default; 12 | 13 | //$body-bg: $light !default; 14 | $body-bg: #fff !default; 15 | $body-color: $dark !default; 16 | 17 | $color-contrast-dark: $dark !default; 18 | $color-contrast-light: $light !default; 19 | 20 | $light-black: rgba($dark, .24) !default; 21 | $light-mix: rgba(mix($light, $dark, 64%), .24) !default; 22 | $light-white: rgba($light, .24) !default; 23 | 24 | $min-black: rgba($dark, .024) !default; 25 | $min-white: rgba(mix($light, $dark, 48%), .1) !default; 26 | 27 | $gray-50: #fbfbfd !default; 28 | $gray-100: $light !default; 29 | $gray-200: mix($light, $dark, 98%) !default; 30 | $gray-300: mix($light, $dark, 94%) !default; 31 | 32 | //$gray-400: mix($light, $dark, 88%) !default; // use default $gray-400 to match bootstrap5 form-control border 33 | 34 | $gray-500: mix($light, $dark, 78%) !default; // #c6cad0 35 | $gray-600: mix($light, $dark, 60%) !default; 36 | $gray-700: mix($light, $dark, 36%) !default; // #6e7680 37 | $gray-800: mix($light, $dark, 16%) !default; 38 | $gray-900: $dark !default; 39 | 40 | $blue: #206bc4 !default; 41 | $azure: #4299e1 !default; 42 | $indigo: #4263eb !default; 43 | $purple: #ae3ec9 !default; 44 | $pink: #d6336c !default; 45 | $red: #d63939 !default; 46 | $orange: #f76707 !default; 47 | $yellow: #f59f00 !default; 48 | $lime: #74b816 !default; 49 | $green: #2fb344 !default; 50 | $teal: #0ca678 !default; 51 | $cyan: #17a2b8 !default; 52 | $black: #000000 !default; 53 | $white: #ffffff !default; 54 | 55 | $text-muted: mix($body-color, #ffffff, percentage($text-muted-opacity)) !default; // #656d77 56 | $text-muted-light: mix($body-color, #ffffff, percentage($text-muted-light-opacity)) !default; 57 | $text-muted-dark: mix($body-color, #ffffff, percentage($text-muted-dark-opacity)) !default; 58 | 59 | $border-color: mix($text-muted, #ffffff, percentage($border-opacity)) !default; 60 | $border-color-transparent: rgba($text-muted, $border-opacity) !default; 61 | 62 | $border-color-dark: mix($text-muted, #ffffff, percentage($border-dark-opacity)) !default; 63 | $border-color-dark-transparent: rgba($text-muted, $border-dark-opacity) !default; 64 | 65 | $active-bg: rgba($blue, .06) !default; 66 | $hover-bg: rgba($text-muted, .06) !default; 67 | 68 | $primary: $blue !default; 69 | $secondary: $text-muted !default; 70 | $success: $green !default; 71 | $info: $azure !default; 72 | $warning: $orange !default; 73 | $danger: $red !default; 74 | 75 | //$code-color: $primary !default; 76 | 77 | 78 | // Dark mode 79 | $dark-mode-darken: darken($dark, 2%) !default; 80 | $dark-mode-lighten: lighten($dark, 2%) !default; 81 | $dark-mode-text: $light; 82 | 83 | // Borders 84 | $border-width: 1px !default; 85 | $border-width-wide: 2px !default; 86 | 87 | //$border-radius-sm: 2px !default; 88 | //$border-radius: 4px !default; 89 | //$border-radius-lg: 8px !default; 90 | $border-radius-pill: 100rem !default; 91 | 92 | 93 | //cards 94 | $card-title-spacer-y: 1.25rem !default; 95 | 96 | $card-border-width: $border-width !default; 97 | $card-border-color: $border-color-transparent !default; 98 | //$card-border-radius: $border-radius !default; 99 | 100 | $card-cap-bg: $white !default; 101 | $card-cap-color: $text-muted !default; 102 | $card-cap-padding-x: 1rem !default; 103 | $card-cap-padding-y: .75rem !default; 104 | 105 | $card-spacer-x: 1rem !default; 106 | 107 | $card-status-size: $border-width-wide !default; 108 | $card-group-margin: 1.5rem !default; 109 | 110 | $card-shadow: rgba($dark, .04) 0 2px 4px 0 !default; 111 | $card-shadow-hover: rgba($dark, .16) 0 2px 16px 0 !default; 112 | 113 | $cards-grid-gap: 1rem !default; 114 | $cards-grid-breakpoint: lg !default; 115 | 116 | 117 | //tables 118 | $table-color: inherit !default; 119 | $table-border-color: $border-color-transparent !default; 120 | $table-head-border-color: $border-color-transparent !default; 121 | $table-head-padding-y: .5rem !default; 122 | $table-head-color: $text-muted !default; 123 | $table-head-bg: $light !default; 124 | $table-striped-order: odd !default; 125 | $table-striped-bg: $light !default; 126 | $table-group-separator-color: $border-color-transparent !default; 127 | -------------------------------------------------------------------------------- /doc_src/data/helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | currentYear() { 3 | const today = new Date(); 4 | return today.getFullYear(); 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /doc_src/img/peopleforce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RemoteDevForce/tom-select/865932f5bb0a8eb47826a907f11452e2301c46f0/doc_src/img/peopleforce.png -------------------------------------------------------------------------------- /doc_src/includes/layout_default.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% if title %} {{ title }} - {% endif %} Tom Select 6 | 7 | 8 | 9 | 10 | {{ style | safe }} 11 | 12 | {% block tomselectjs %} 13 | 14 | {% endblock %} 15 | 16 | 17 | {{ script | safe }} 18 | 19 | 20 | 21 | 22 | 23 | {# offcanvas button #} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {# offcanvas #} 32 | 33 | 34 | 35 | 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 | 8 | {% for item in collections.plugins %} 9 | {% nav_link item %} 10 | {% endfor %} 11 | 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 | {{ heading }} 7 | Description 8 | 9 | 10 | {% for row in data %} 11 | {{ config_row( row.name, row.desc, row.type, row.default, row.highlight) }} 12 | {% endfor %} 13 | 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 | 13 | Make sure tests passRun npm test to make sure your changes don't break existing functionality 14 | Do not make changes to files in /dist Limiting your edits to files in /src or /doc_src directories keeps the size of your pull request down and makes it easier for us to evaluate. We'll update the /dist folder after your pull request is approved. 15 | Add testsIn the best case scenario, you are also adding tests to back up your changes, but don't sweat it if you don't. We can discuss them at a later date. 16 | 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 | Event 26 | Params 27 | Description 28 | 29 | 30 | 31 | "initialize" 32 | 33 | Invoked once the control is completely initialized. 34 | 35 | 36 | "change" 37 | value 38 | Invoked when the value of the control changes. 39 | 40 | 41 | "focus" 42 | 43 | Invoked when the control gains focus. 44 | 45 | 46 | "blur" 47 | 48 | Invoked when the control loses focus. 49 | 50 | 51 | "item_add" 52 | value, item 53 | Invoked when an item is added (i.e., when an option is selected) 54 | 55 | 56 | "item_remove" 57 | value, $item 58 | Invoked when an item is deselected. 59 | 60 | 61 | "item_select" 62 | item 63 | Invoked when an item is selected. 64 | 65 | 66 | "clear" 67 | 68 | Invoked when the control is manually cleared via the clear() method. 69 | 70 | 71 | "option_add" 72 | value, data 73 | Invoked when a new option is added to the available options list. 74 | 75 | 76 | "option_remove" 77 | value 78 | Invoked when an option is removed from the available options. 79 | 80 | 81 | "option_clear" 82 | 83 | Invoked when all options are removed from the control. 84 | 85 | 86 | "optgroup_add" 87 | id, data 88 | Invoked when a new option is added to the available options list. 89 | 90 | 91 | "optgroup_remove" 92 | id 93 | Invoked when an option group is removed. 94 | 95 | 96 | "optgroup_clear" 97 | 98 | Invoked when all option groups are removed. 99 | 100 | 101 | "dropdown_open" 102 | dropdown 103 | Invoked when the dropdown opens. 104 | 105 | 106 | "dropdown_close" 107 | dropdown 108 | Invoked when the dropdown closes. 109 | 110 | 111 | "type" 112 | str 113 | Invoked when the user types while filtering options. 114 | 115 | 116 | "load" 117 | data 118 | Invoked when new options have been loaded and added to the control (via the load option or load API method). 119 | 120 | 121 | "destroy" 122 | 123 | Invoked right before the control is destroyed. 124 | 125 | 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 | 11 | API 12 | 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 | Email Contacts 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 | 13 | Regex Filter 14 | 15 | {% endset %} 16 | 17 | {% set html %} 18 | 19 | 20 | Pattern 21 | 22 | 23 | {% endset %} 24 | 25 | 33 | 34 | {{ demo( label, html, script, style) }} 35 | 36 | {% set label %} 37 | 38 | Minimum Length 39 | 40 | {% endset %} 41 | 42 | {% set html %} 43 | 44 | 45 | Minimum length 46 | 47 | 48 | {% endset %} 49 | 50 | 58 | 59 | {{ demo( label, html, script, style) }} 60 | 61 | 62 | {% set label %} 63 | 64 | Unique Words 65 | 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 | 16 | Custom Option and Item HTML 17 | 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 | 76 | Custom JavaScript 77 | 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 | 87 | How cool is this? 88 | amazing 89 | awesome 90 | cool 91 | excellent 92 | great 93 | neat 94 | superb 95 | wonderful 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 | 11 | Event Logger 12 | 13 | Check out the console for more details about each event. 14 | {% endset %} 15 | 16 | {% set html %} 17 | 18 | Select a state... 19 | Alabama 20 | Alaska 21 | Arizona 22 | Arkansas 23 | California 24 | Colorado 25 | Connecticut 26 | Delaware 27 | District of Columbia 28 | Florida 29 | Georgia 30 | Hawaii 31 | Idaho 32 | Illinois 33 | Indiana 34 | Iowa 35 | Kansas 36 | Kentucky 37 | Louisiana 38 | Maine 39 | Maryland 40 | Massachusetts 41 | Michigan 42 | Minnesota 43 | Mississippi 44 | Missouri 45 | Montana 46 | Nebraska 47 | Nevada 48 | New Hampshire 49 | New Jersey 50 | New Mexico 51 | New York 52 | North Carolina 53 | North Dakota 54 | Ohio 55 | Oklahoma 56 | Oregon 57 | Pennsylvania 58 | Rhode Island 59 | South Carolina 60 | South Dakota 61 | Tennessee 62 | Texas 63 | Utah 64 | Vermont 65 | Virginia 66 | Washington 67 | West Virginia 68 | Wisconsin 69 | Wyoming 70 | 71 | Event Log 72 | 73 | {% endset %} 74 | 75 | 98 | 99 | {{ demo( label, html, script ) }} 100 | 101 | 102 | 103 | {% set label %} 104 | 105 | onDelete Prompt 106 | 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 | 13 | Internationalization 14 | 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 | 23 | How cool is this? 24 | amazing 25 | awesome 26 | cool 27 | excellent 28 | great 29 | neat 30 | superb 31 | wonderful 32 | {% endset %} 33 | 34 | 35 | 51 | 52 | 53 | {{ demo( label, html, script, style) }} 54 | 55 | 56 | {% set label %} 57 | 58 | RTL Input 59 | 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 | 86 | RTL Select 87 | 88 | {% endset %} 89 | 90 | {% set html %} 91 | 92 | Select a person... 93 | Thomas Edison 94 | Nikola 95 | Nikola Tesla 96 | Arnold Schwarzenegger 97 | 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 | 13 | Locked (single) 14 | 15 | Controls can be locked to prevent user interaction. 16 | {% endset %} 17 | 18 | {% set html %} 19 | 20 | Option A 21 | Option B 22 | Option C 23 | 24 | {% endset %} 25 | 26 | 31 | 32 | {{ demo( label, html, script) }} 33 | 34 | 35 | {% set label %} 36 | 37 | Locked (multiple) 38 | 39 | {% endset %} 40 | 41 | {% set html %} 42 | 43 | Option A 44 | Option B 45 | Option C 46 | 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 | 12 | JavaScript Array 13 | 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 | Data-* Attributes 46 | Images can be added to option and item elements with custom render templates and data-* attributes 47 | {% endset %} 48 | 49 | {% set html %} 50 | 51 | Google Chrome 52 | Mozilla Firefox 53 | Internet Explorer 54 | 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 | 13 | Performance 14 | 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 | 18 | Example 19 | 20 | {% endset %} 21 | 22 | {% set html %} 23 | 24 | amazing 25 | awesome 26 | cool 27 | excellent 28 | great 29 | neat 30 | superb 31 | wonderful 32 | 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 | Example 14 | Listen for 'change' events on the original input/select and update Tom Select accordingly 15 | {% endset %} 16 | 17 | {% set html %} 18 | 19 | Change original input 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 | 13 | Example 14 | 15 | {% endset %} 16 | 17 | {% set html %} 18 | 19 | amazing 20 | awesome 21 | cool 22 | excellent 23 | great 24 | neat 25 | superb 26 | wonderful 27 | 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 | 14 | Select Multiple 15 | 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 | 43 | Select Single 44 | 45 | {% endset %} 46 | 47 | 48 | {% set html %} 49 | 50 | How cool is this? 51 | awesome 52 | neat 53 | 54 | {% endset %} 55 | 56 | 66 | 67 | 68 | {{ demo( label, html, script) }} 69 | 70 | 71 | {% set label %} 72 | 73 | Select Single with Empty Option 74 | 75 | {% endset %} 76 | 77 | 78 | {% set html %} 79 | 80 | How cool is this? 81 | awesome 82 | neat 83 | 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 | 17 | Example 18 | 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 | 15 | Example 16 | 17 | {% endset %} 18 | 19 | {% set html %} 20 | 21 | Text 22 | Markdown 23 | HTML 24 | PHP 25 | Python 26 | Java 27 | JavaScript 28 | Ruby 29 | VHDL 30 | Verilog 31 | C# 32 | C/C++ 33 | 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 | 13 | Example 14 | 15 | {% endset %} 16 | 17 | {% set html %} 18 | 19 | amazing 20 | awesome 21 | cool 22 | excellent 23 | great 24 | neat 25 | superb 26 | wonderful 27 | 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 | 15 | Example 16 | 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 | 25 | amazing 26 | awesome 27 | cool 28 | excellent 29 | great 30 | neat 31 | superb 32 | wonderful 33 | 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 | 13 | Example 14 | 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 | 13 | Example with remove_button & no_backspace_delete 14 | 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 | 15 | Example 16 | 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 | 17 | Example - Multiple 18 | 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 | 53 | Example - Single 54 | 55 | 56 | For single selects, you may prefer the Clear Button plugin 57 | 58 | {% endset %} 59 | 60 | {% set html %} 61 | 62 | awesome 63 | neat 64 | 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 | 15 | Example 16 | 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 | 15 | Example 16 | 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 | 83 | firstUrl(query_string) should return the request url for the first `page` of data. 84 | Call setNextUrl(query_string,next_url) in your load() method upon request completion to define subsequent request urls. 85 | Use getUrl(query_string) to retrieve the appropriate pagination url for the given query string. 86 | ', 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 | 15 | 16 | Dropdown button 17 | 18 | 19 | Action 20 | Another action 21 | 22 | 23 | 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 | label 26 | 27 | 28 | One 29 | Two 30 | 31 | 32 | One 33 | Two 34 | 35 | 36 | 37 | 38 | 39 | 40 | submit 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 : 'abc', 39 | AB_Single : 'abc', 40 | AB_Single_Long : 'abcdefghijklmnop', 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 | 4 | 5 | a 6 | b 7 | c 8 | 9 | `; 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('aa',{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('',{ 55 | load: function(query, load_cb){ 56 | 57 | assert.equal(query, 'c'); 58 | setTimeout(function(){ 59 | 60 | load_cb([{value: 'c', text: 'C'}]); 61 | 62 | setTimeout(function(){ 63 | expect(test.instance.isOpen).to.be.equal(true); 64 | expect(test.instance.dropdown.querySelectorAll('.no-results').length).to.be.equal(0); 65 | done(); 66 | },200); 67 | 68 | },200); 69 | } 70 | }); 71 | 72 | click(test.instance.control, function(){ 73 | syn.type('c', test.instance.control_input,function(){ 74 | 75 | setTimeout(function(){ 76 | expect(test.instance.dropdown.querySelectorAll('.no-results').length).to.be.equal(0); 77 | },100); 78 | 79 | }); 80 | }); 81 | 82 | }); 83 | 84 | 85 | it_n('should load results for "a" after loading results for "ab"', function(done) { 86 | 87 | var expected_load_queries = ['ab','a']; 88 | 89 | var test = setup_test('AB_Single',{ 90 | load: function(query, load_cb) { 91 | 92 | var expected_load_query = expected_load_queries.shift(); 93 | 94 | assert.equal(query, expected_load_query); 95 | 96 | if( expected_load_queries.length == 0 ){ 97 | done(); 98 | } 99 | 100 | return load_cb(); 101 | } 102 | }); 103 | 104 | click(test.instance.control, function(){ 105 | syn.type('a', test.instance.control_input,function(){ 106 | assert.equal(test.instance.loading,1); 107 | syn.type('b', test.instance.control_input,function(){ 108 | assert.equal(test.instance.loading,1); 109 | setTimeout(function(){ 110 | syn.type('\b', test.instance.control_input,function(){ 111 | assert.equal(test.instance.loading,1); 112 | }); 113 | },400); // greater than load throttle 114 | }); 115 | }); 116 | }); 117 | 118 | }); 119 | 120 | 121 | it_n('dropdown should not open if query length is less', function(done) { 122 | 123 | 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(` 108 | None 109 | Thomas Edison 110 | Nikola 111 | `, {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(''); 12 | assert.isTrue(true); 13 | 14 | }).catch(function(err){ 15 | assert.fail('import tom-select.complete.js failed'); 16 | 17 | }); 18 | }); 19 | 20 | 21 | it_n('isKeyDown', async() => { 22 | 23 | var last_keydown; 24 | document.body.addEventListener('keydown',function(evt){ 25 | last_keydown = evt; 26 | }); 27 | 28 | const util_module = await import('/base/build/esm/utils.js'); 29 | 30 | await asyncType('[alt]', document.body); 31 | await waitFor(100); 32 | assert.equal( util_module.isKeyDown('shiftKey',last_keydown), false, 'should return false if [alt] is pressed'); 33 | await asyncType('[alt-up]', document.body); 34 | 35 | await asyncType('[alt][shift]', document.body); 36 | await waitFor(100); 37 | assert.equal( util_module.isKeyDown('shiftKey',last_keydown), false, 'should return false if [alt][shift] is pressed'); 38 | await asyncType('[alt-up][shift-up]', document.body); 39 | 40 | 41 | await asyncType('['+shortcut_key+'][shift]', document.body); 42 | await waitFor(100); 43 | assert.equal( util_module.isKeyDown('shiftKey',last_keydown), false, 'should return false if ['+shortcut_key+'][shift] is pressed'); 44 | assert.equal( util_module.isKeyDown('ctrlKey',last_keydown), false, 'should return false if ['+shortcut_key+'][shift] is pressed'); 45 | await asyncType('['+shortcut_key+'-up][shift-up]', document.body); 46 | 47 | 48 | await asyncType('[shift]', document.body); 49 | await waitFor(100); 50 | assert.equal( util_module.isKeyDown('shiftKey',last_keydown), true, 'should return true if [shift] is pressed'); 51 | await asyncType('[shift-up]', document.body); 52 | 53 | 54 | await asyncType('['+shortcut_key+']', document.body); 55 | await waitFor(100); 56 | assert.equal( util_module.isKeyDown(shortcut_key+'Key',last_keydown), true, 'should return true if ['+shortcut_key+'] is pressed'); 57 | await asyncType('['+shortcut_key+'-up]', document.body); 58 | 59 | 60 | 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /test/tests/events_dom.js: -------------------------------------------------------------------------------- 1 | describe('DOM Events', function() { 2 | describe('"change"', function() { 3 | it_n('should be triggered once by addItem()', function(done) { 4 | var test = setup_test('', { 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('', { 25 | valueField: 'value', 26 | labelField: 'value', 27 | options: [ 28 | {value: 'a'}, 29 | {value: 'b'}, 30 | ], 31 | items: ['a','b'] 32 | }); 33 | 34 | var counter = 0; 35 | test.instance.on('change', function() { counter++; }); 36 | test.instance.removeItem('b'); 37 | 38 | window.setTimeout(function() { 39 | expect(counter).to.be.equal(1); 40 | done(); 41 | }, 0); 42 | }); 43 | it_n('should be triggered once by clear()', function(done) { 44 | 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(' added on original ', async ()=>{ 58 | 59 | let test = setup_test('AB_Multi', {plugins: ['change_listener']}); 60 | var input = test.select; 61 | var opt = new Option('new', 'new', true, true); 62 | test.select.append(opt); 63 | 64 | changeInput(input); 65 | await waitFor(10); 66 | 67 | assert.equal(test.instance.items.length, 1,'should have one item'); 68 | assert.equal(test.instance.items[0], 'new'); 69 | 70 | }); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /test/tests/plugins/checkbox_options.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | describe('plugin: checkbox_options', function() { 5 | 6 | it_n('active items should be checked on load', function(done) { 7 | 8 | let test = setup_test('', {plugins: ['checkbox_options']}); 9 | 10 | click(test.instance.control,function(){ 11 | var checked = test.instance.dropdown.querySelectorAll('input:checked'); 12 | assert.equal(checked.length,2); 13 | done(); 14 | }); 15 | 16 | }); 17 | 18 | it_n('checkbox should be updated after option is clicked', function(done) { 19 | 20 | let test = setup_test('AB_Multi', {plugins: ['checkbox_options']}); 21 | click(test.instance.control_input,function(){ 22 | 23 | var option = test.instance.getOption('a'); 24 | var checkbox = option.querySelector('input'); 25 | 26 | // check/active 27 | click(option,function(){ 28 | assert.deepEqual(test.instance.items, ['a']); 29 | assert.equal(checkbox.checked, true,'checkbox not checked'); 30 | 31 | // uncheck 32 | click(option,function(){ 33 | assert.deepEqual(test.instance.items, []); 34 | assert.equal(checkbox.checked, false,'checkbox checked'); 35 | done(); 36 | 37 | }); 38 | 39 | }); 40 | 41 | }); 42 | 43 | }); 44 | 45 | it_n('checkbox should be checked after checkbox is clicked', function(done) { 46 | 47 | let test = setup_test('AB_Multi', {plugins: ['checkbox_options']}); 48 | click(test.instance.control_input,function(){ 49 | 50 | var option = test.instance.getOption('a'); 51 | var checkbox = option.querySelector('input'); 52 | 53 | // check/active 54 | click(checkbox,function(){ 55 | assert.deepEqual(test.instance.items, ['a']); 56 | assert.equal(checkbox.checked, true,'checkbox not checked'); 57 | 58 | // uncheck 59 | click(checkbox,function(){ 60 | assert.deepEqual(test.instance.items, []); 61 | assert.equal(checkbox.checked, false,'checkbox checked'); 62 | done(); 63 | 64 | }); 65 | }); 66 | }); 67 | }); 68 | 69 | 70 | it_n('removing item before dropdown open should not check option', function(done) { 71 | 72 | let test = setup_test('', {plugins: ['checkbox_options']}); 73 | test.instance.removeItem('a'); 74 | 75 | click(test.instance.control_input,function(){ 76 | var option = test.instance.getOption('a'); 77 | var checkbox = option.querySelector('input'); 78 | assert.equal(checkbox.checked, false,'checkbox checked'); 79 | done(); 80 | }); 81 | 82 | }); 83 | 84 | 85 | it_n('adding item after dropdown open should check option', async () => { 86 | 87 | let test = setup_test('AB_Multi', {plugins: ['checkbox_options']}); 88 | 89 | await asyncClick( test.instance.control ); 90 | 91 | var option = test.instance.getOption('a'); 92 | var checkbox = option.querySelector('input'); 93 | assert.equal(checkbox.checked, false,'checkbox should not be checked'); 94 | 95 | test.instance.addItem('a'); 96 | 97 | await waitFor(100); // setTimeout in UpdateCheckbox 98 | 99 | //var option = test.instance.getOption('a'); 100 | //var checkbox = option.querySelector('input'); 101 | 102 | assert.equal(checkbox.checked, true,'checkbox should be checked'); 103 | 104 | }); 105 | 106 | it_n('creating item should check option', async () => { 107 | 108 | let test = setup_test('AB_Multi', { 109 | create: true, 110 | plugins: ['checkbox_options'] 111 | }); 112 | 113 | await asyncClick( test.instance.control ); 114 | await asyncType( 'new-value' ); 115 | await asyncType( '[enter]' ); 116 | 117 | await waitFor(100); // setTimeout in UpdateCheckbox 118 | 119 | var option = test.instance.getOption('new-value'); 120 | var checkbox = option.querySelector('input'); 121 | assert.equal(checkbox.checked, true,'checkbox should be checked'); 122 | }); 123 | 124 | 125 | }); 126 | -------------------------------------------------------------------------------- /test/tests/plugins/clear_button.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | describe('plugin: clear_button', function() { 5 | 6 | it_n('should remove all item when button is clicked', function(done) { 7 | 8 | var on_change_calls = 0; 9 | let test = setup_test('AB_Multi', { 10 | plugins: ['clear_button'], 11 | onChange:function(){ 12 | on_change_calls++; 13 | } 14 | }); 15 | 16 | test.instance.addItem('a'); 17 | test.instance.addItem('b'); 18 | 19 | on_change_calls = 0; 20 | assert.equal( test.instance.items.length, 2 ); 21 | 22 | var button = test.instance.control.querySelector('.clear-button'); 23 | 24 | syn.click( button, function() { 25 | assert.equal( test.instance.items.length, 0, 'should clear items array' ); 26 | assert.equal( on_change_calls, 1,'should only call onChange once' ); 27 | done(); 28 | }); 29 | 30 | }); 31 | 32 | it_n('single with empty option', async () => { 33 | 34 | let test = setup_test('emptyabc', 35 | { 36 | allowEmptyOption: true, 37 | plugins: ['clear_button'] 38 | } 39 | ); 40 | 41 | var button = test.instance.control.querySelector('.clear-button'); 42 | 43 | // initialize with empty option 44 | var empty_item = test.instance.getItem(''); 45 | assert.isNotOk( empty_item.querySelector('.remove'), 'empty option should not have remove button' ); 46 | assert.isFalse(test.instance.isValid,'should start out as invalid'); 47 | assert.equal( test.instance.items.length,1); 48 | assert.equal( test.instance.items[0],''); 49 | 50 | 51 | // select "a" 52 | await asyncClick( test.instance.control ); 53 | var option = test.instance.dropdown_content.querySelector('[data-value="a"]'); 54 | await asyncClick( option ); 55 | var itema = test.instance.getItem('a'); 56 | assert.isOk( itema,'should have item "a"'); 57 | assert.equal( test.instance.items.length,1); 58 | assert.equal( test.instance.items[0],['a']); 59 | assert.isTrue( test.instance.isValid, 'should be valid'); 60 | 61 | // remove item "a" 62 | await asyncClick( button ); 63 | assert.equal( test.instance.items.length,1); 64 | assert.equal( test.instance.items[0],''); 65 | assert.isFalse( test.instance.isValid, 'should not be valid'); 66 | 67 | }); 68 | 69 | 70 | it_n('should not clear if disabled', async () => { 71 | 72 | let test = setup_test('', 73 | { 74 | plugins: ['clear_button'] 75 | } 76 | ); 77 | 78 | var button = test.instance.control.querySelector('.clear-button'); 79 | 80 | await asyncClick( button ); 81 | assert.equal( test.instance.items.length,3); 82 | 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /test/tests/plugins/dropdown_header.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | describe('plugin: dropdown_header', function() { 4 | 5 | it_n('header should be added to dropdown menu', function() { 6 | 7 | var test = setup_test('AB_Multi',{plugins:['dropdown_header']}); 8 | var header = test.instance.dropdown.querySelectorAll('.dropdown-header'); 9 | 10 | expect(header.length).to.be.equal(1); 11 | }); 12 | 13 | it_n('dropdown should close when clicking on close button', function(done) { 14 | 15 | var test = setup_test('AB_Multi',{plugins:['dropdown_header']}); 16 | var header = test.instance.dropdown.querySelector('.dropdown-header'); 17 | var button = header.querySelector('.dropdown-header-close'); 18 | 19 | click(test.instance.control, function(){ 20 | assert.equal(test.instance.isOpen,true); 21 | click(button,function(){ 22 | assert.equal(test.instance.isOpen,false); 23 | done(); 24 | }); 25 | 26 | }); 27 | 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /test/tests/plugins/input_autogrow.js: -------------------------------------------------------------------------------- 1 | 2 | describe('plugin: input_autogrow', function() { 3 | 4 | it_n('width of control should change as text changes', function(done) { 5 | 6 | let test = setup_test('', {plugins: ['input_autogrow']}); 7 | 8 | syn.type('a', test.instance.control_input, function() { 9 | let width_orig = test.instance.control_input.clientWidth; 10 | 11 | syn.type('a', test.instance.control_input, function() { 12 | let width_now = test.instance.control_input.clientWidth; 13 | 14 | expect(width_now).to.be.above(width_orig); 15 | done(); 16 | }); 17 | }); 18 | 19 | }); 20 | 21 | it_n('width of control should not change without selected items', function(done) { 22 | 23 | let test = setup_test('', {plugins: ['input_autogrow']}); 24 | 25 | syn.type('a', test.instance.control_input, function() { 26 | let width_orig = test.instance.control_input.clientWidth; 27 | 28 | syn.type('a', test.instance.control_input, function() { 29 | let width_now = test.instance.control_input.clientWidth; 30 | 31 | assert.equal(width_now,width_orig); 32 | done(); 33 | }); 34 | }); 35 | 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /test/tests/plugins/invalid_plugin.js: -------------------------------------------------------------------------------- 1 | describe('plugin: invalid', function() { 2 | 3 | it_n('throw error when attempting to load invalid plugin', function() { 4 | 5 | let errors = 0 ; 6 | try { 7 | setup_test('', {plugins: ['invalid']}); 8 | } catch (error) { 9 | errors++; 10 | } 11 | 12 | assert.equal(errors,1); 13 | }); 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /test/tests/plugins/no_active_items.js: -------------------------------------------------------------------------------- 1 | 2 | describe('plugin: no_active_items', function() { 3 | 4 | it_n('should not activate item on click', function(done) { 5 | 6 | let test = setup_test('', {plugins: ['no_active_items']}); 7 | var item = test.instance.getItem('a'); 8 | 9 | assert.equal( test.instance.items.length, 2 , 'no items' ); 10 | 11 | click(item,function(){ 12 | assert.equal( test.instance.activeItems.length, 0 , 'has active item' ); 13 | done(); 14 | }); 15 | 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /test/tests/plugins/no_backspace_delete.js: -------------------------------------------------------------------------------- 1 | 2 | describe('plugin: no_backspace_delete', function() { 3 | 4 | it_n('should not delete item on backspace', function(done) { 5 | 6 | let test = setup_test('', {plugins: ['no_backspace_delete']}); 7 | 8 | assert.equal( test.instance.items.length, 2 ); 9 | assert.equal( test.instance.activeItems.length, 0, 'no active items' ); 10 | 11 | syn.click(test.instance.control_input,function(){ 12 | syn.type('\b', test.instance.control_input, function() { 13 | 14 | assert.equal( test.instance.items.length, 2, 'item was deleted'); 15 | done(); 16 | 17 | }); 18 | }); 19 | 20 | }); 21 | 22 | it_n('should delete active item on backspace', function(done) { 23 | 24 | let test = setup_test('', {plugins: ['no_backspace_delete']}); 25 | 26 | syn.click(test.instance.control_input,function(){ 27 | 28 | test.instance.setActiveItem(test.instance.getItem('b')); 29 | assert.equal( test.instance.items.length, 2 ); 30 | assert.equal( test.instance.activeItems.length, 1, 'no active items' ); 31 | 32 | syn.type('\b', test.instance.control_input, function() { 33 | 34 | assert.equal( test.instance.items.length, 1, 'item not deleted' ); 35 | done(); 36 | 37 | }); 38 | }); 39 | 40 | }); 41 | 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /test/tests/plugins/optgroup_columns.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | describe('plugin: optgroup_columns', function() { 4 | 5 | var optgroup_test = function(){ 6 | return setup_test('',{ 7 | options: [ 8 | {id: 'avenger', make: 'dodge', model: 'Avenger'}, 9 | {id: 'caliber', make: 'dodge', model: 'Caliber'}, 10 | {id: 'caravan-grand-passenger', make: 'dodge', model: 'Caravan Grand Passenger'}, 11 | {id: 'challenger', make: 'dodge', model: 'Challenger'}, 12 | {id: 'ram-1500', make: 'dodge', model: 'Ram 1500'}, 13 | {id: 'viper', make: 'dodge', model: 'Viper'}, 14 | {id: 'a3', make: 'audi', model: 'A3'}, 15 | {id: 'a6', make: 'audi', model: 'A6'}, 16 | {id: 'r8', make: 'audi', model: 'R8'}, 17 | {id: 'rs-4', make: 'audi', model: 'RS 4'}, 18 | {id: 's4', make: 'audi', model: 'S4'}, 19 | {id: 's8', make: 'audi', model: 'S8'}, 20 | {id: 'tt', make: 'audi', model: 'TT'}, 21 | {id: 'avalanche', make: 'chevrolet', model: 'Avalanche'}, 22 | {id: 'aveo', make: 'chevrolet', model: 'Aveo'}, 23 | {id: 'cobalt', make: 'chevrolet', model: 'Cobalt'}, 24 | {id: 'silverado', make: 'chevrolet', model: 'Silverado'}, 25 | {id: 'suburban', make: 'chevrolet', model: 'Suburban'}, 26 | {id: 'tahoe', make: 'chevrolet', model: 'Tahoe'}, 27 | {id: 'trail-blazer', make: 'chevrolet', model: 'TrailBlazer'}, 28 | ], 29 | optgroups: [ 30 | {$order: 3, id: 'dodge', name: 'Dodge'}, 31 | {$order: 2, id: 'audi', name: 'Audi'}, 32 | {$order: 1, id: 'chevrolet', name: 'Chevrolet'} 33 | ], 34 | labelField: 'model', 35 | valueField: 'id', 36 | optgroupField: 'make', 37 | optgroupLabelField: 'name', 38 | optgroupValueField: 'id', 39 | lockOptgroupOrder: true, 40 | searchField: ['model'], 41 | plugins: ['optgroup_columns'], 42 | openOnFocus: false 43 | }); 44 | }; 45 | 46 | 47 | it_n('three optgroups should be displayed', async () => { 48 | 49 | var test = optgroup_test(); 50 | await asyncClick(test.instance.control); 51 | await asyncType('a'); 52 | 53 | var optgroups = test.instance.dropdown_content.querySelectorAll('.optgroup'); 54 | expect(optgroups.length).to.be.equal(3); 55 | 56 | }); 57 | 58 | it_n('[right] keypress should move focus to second optgroup', async () => { 59 | var test = optgroup_test(); 60 | 61 | // 1) move right to audi 62 | await asyncClick(test.instance.control); 63 | await asyncType('a[right]'); 64 | 65 | var optgroup = test.instance.activeOption.parentNode; 66 | expect(optgroup.dataset.group).to.be.equal('audi'); 67 | 68 | // 2) move left to chevy 69 | await asyncType('[left]'); 70 | 71 | var optgroup = test.instance.activeOption.parentNode; 72 | expect(optgroup.dataset.group).to.be.equal('chevrolet'); 73 | 74 | }); 75 | 76 | }); 77 | -------------------------------------------------------------------------------- /test/tests/plugins/remove_button.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | describe('plugin: remove_button', function() { 5 | 6 | it_n('should remove item when remove button is clicked', function(done) { 7 | 8 | let test = setup_test('AB_Multi', {plugins: ['remove_button']}); 9 | 10 | test.instance.addItem('a'); 11 | test.instance.addItem('b'); 12 | assert.equal( test.instance.items.length, 2 ); 13 | 14 | var itema = test.instance.getItem('b'); 15 | var remove_button = itema.querySelector('.remove'); 16 | 17 | syn.click( remove_button, function() { 18 | assert.equal( test.instance.items.length, 1 ); 19 | assert.equal( test.instance.items[0], 'a' ); 20 | done(); 21 | 22 | }); 23 | 24 | }); 25 | 26 | it_n('option should reappear in dropdown when removed', function(done) { 27 | 28 | let test = setup_test('AB_Multi', {plugins: ['remove_button']}); 29 | 30 | test.instance.addItem('a'); 31 | test.instance.addItem('b'); 32 | assert.equal( test.instance.items.length, 2 ); 33 | 34 | syn.click(test.instance.control_input,function(){ 35 | 36 | assert.equal( test.instance.dropdown_content.querySelectorAll('.option').length, 1); 37 | 38 | var itema = test.instance.getItem('b'); 39 | var remove_button = itema.querySelector('.remove'); 40 | 41 | syn.click( remove_button, function() { 42 | assert.equal( test.instance.dropdown_content.querySelectorAll('.option').length, 2); 43 | done(); 44 | }); 45 | 46 | }); 47 | 48 | }); 49 | 50 | 51 | it_n('rendering item a second time should not add a remove button a second time', function() { 52 | 53 | let test = setup_test('AB_Multi', {plugins: ['remove_button']}); 54 | 55 | 56 | var item = test.instance.render('item', test.instance.options['a']); 57 | item = test.instance.render('item', test.instance.options['a']); 58 | 59 | assert.equal( item.querySelectorAll('.remove').length, 1); 60 | 61 | }); 62 | 63 | 64 | it_n('should not remove item if locked', function(done) { 65 | 66 | let test = setup_test('AB_Multi', {plugins: ['remove_button']}); 67 | 68 | test.instance.addItem('a'); 69 | test.instance.addItem('b'); 70 | test.instance.lock(); 71 | assert.equal( test.instance.items.length, 2 ); 72 | 73 | var itema = test.instance.getItem('b'); 74 | var remove_button = itema.querySelector('.remove'); 75 | 76 | syn.click( remove_button, function() { 77 | assert.equal( test.instance.items.length, 2 ); 78 | done(); 79 | }); 80 | 81 | }); 82 | 83 | it_n('remove_button options', function(done) { 84 | 85 | let config = { 86 | plugins: { 87 | remove_button:{ 88 | className: 'customclass', 89 | } 90 | } 91 | 92 | } 93 | 94 | let test = setup_test('ab', config); 95 | var itema = test.instance.getItem('a'); 96 | var remove_button = itema.querySelector('.customclass'); 97 | 98 | assert.equal( test.instance.items.length, 1 ); 99 | 100 | syn.click( remove_button, function() { 101 | assert.equal( test.instance.items.length, 0 ); 102 | done(); 103 | }); 104 | 105 | }); 106 | 107 | it_n('should not remove item when onDelete returns false', async () => { 108 | 109 | let test = setup_test('AB_Multi', { 110 | plugins: ['remove_button'], 111 | onDelete: () => false, 112 | }); 113 | 114 | test.instance.addItem('a'); 115 | test.instance.addItem('b'); 116 | test.instance.lock(); 117 | assert.equal( test.instance.items.length, 2 ); 118 | 119 | var itema = test.instance.getItem('b'); 120 | var remove_button = itema.querySelector('.remove'); 121 | 122 | await asyncClick(remove_button); 123 | await waitFor(100); 124 | assert.equal( test.instance.items.length, 2 ); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/tests/plugins/restore_on_backspace.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | describe('plugin: restore_on_backspace', function() { 4 | 5 | it_n('should fill control_input.value when item deleted', function(done) { 6 | 7 | var test = setup_test('AB_Multi',{plugins:['restore_on_backspace']}); 8 | 9 | test.instance.addItem('a'); 10 | test.instance.addItem('b'); 11 | assert.equal( test.instance.items.length, 2 ); 12 | 13 | syn.click(test.instance.control_input,function(){ 14 | syn.type('a\b\b', test.instance.control_input, function() { 15 | assert.equal( test.instance.items.length, 1 ); 16 | assert.equal( test.instance.items[0], 'a' ); 17 | assert.equal( test.instance.control_input.value, 'b' ); 18 | done(); 19 | 20 | }); 21 | }); 22 | 23 | }); 24 | 25 | it_n('should fill control_input.value when active item deleted', function(done) { 26 | 27 | var test = setup_test('AB_Multi',{hidePlaceholder:true,plugins:['restore_on_backspace']}); 28 | 29 | click(test.instance.control_input,function(){ 30 | 31 | test.instance.addItem('a'); 32 | test.instance.addItem('b'); 33 | assert.equal( test.instance.items.length, 2 ); 34 | test.instance.setActiveItem(test.instance.getItem('a')); 35 | 36 | syn.type('\b', test.instance.control_input, function() { 37 | assert.equal( test.instance.items.length, 1, 'items.length not 1' ); 38 | assert.equal( test.instance.items[0], 'b', 'first item not b' ); 39 | assert.equal( test.instance.control_input.value, 'a', 'input value not a'); 40 | done(); 41 | 42 | }); 43 | }); 44 | 45 | }); 46 | 47 | 48 | it_n('should not fill control_input.value if not focused', async () => { 49 | 50 | var test = setup_test('',{hidePlaceholder:true,plugins:['restore_on_backspace','clear_button']}); 51 | var button = test.instance.control.querySelector('.clear-button'); 52 | 53 | await asyncClick( button ); 54 | assert.equal( test.instance.control_input.value, '', 'control_input is not empty'); 55 | 56 | }); 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /test/tests/xss.js: -------------------------------------------------------------------------------- 1 | 2 | var setup_xss_test = function(html, options, done) { 3 | window.xss = function() { 4 | window.clearTimeout(timeout); 5 | complete(new Error('Exploit executed')); 6 | }; 7 | 8 | var test = setup_test(html, options); 9 | var complete = function(err) { 10 | window.xss = function() {}; 11 | done(err); 12 | }; 13 | var timeout = window.setTimeout(complete, 75); 14 | return test; 15 | }; 16 | 17 | describe('XSS', function() { 18 | 19 | it_n('Raw HTML in original input value should not trigger exploit', function(done) { 20 | setup_xss_test('', {}, done); 21 | }); 22 | 23 | it_n('Raw HTML in optgroup label should not trigger exploit', function(done) { 24 | var test = setup_xss_test('Test', {}, done); 25 | test.instance.refreshOptions(); 26 | test.instance.open(); 27 | }); 28 | 29 | it_n('Raw HTML in option label should not trigger exploit', function(done) { 30 | setup_xss_test('', { 31 | options: [ 32 | {value: '1', label: ''} 33 | ], 34 | items: ['1'], 35 | }, done); 36 | }); 37 | 38 | it_n('Raw HTML in option value should not trigger exploit', function(done) { 39 | setup_xss_test('', { 40 | options: [ 41 | {value: '', label: '1'} 42 | ], 43 | items: [''], 44 | }, done); 45 | }); 46 | 47 | 48 | it_n('Custom templates should not trigger exploit', function(done) { 49 | setup_xss_test('', { 50 | options: [ 51 | {value:'1',label: ''} 52 | ], 53 | items: ['1'], 54 | render:{ 55 | 'item': function(data, escape) { 56 | return '' + escape(data.label) + ''; 57 | }, 58 | } 59 | }, done); 60 | }); 61 | 62 | }); 63 | --------------------------------------------------------------------------------
{{ setting }}
{{ type }}
{{ default }}
"initialize"
"change"
value
"focus"
"blur"
"item_add"
item
"item_remove"
$item
"item_select"
"clear"
"option_add"
data
"option_remove"
"option_clear"
"optgroup_add"
id
"optgroup_remove"
"optgroup_clear"
"dropdown_open"
dropdown
"dropdown_close"
"type"
str
"load"
load
"destroy"
Examples of how to interact with the control programmatically.
An example showing how you might go about creating contact selector like those used in Email apps.
Search by email address with "email:gmail.com" or last name with "last:tesla"
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 |
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 |
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 |
Check out the console for more details about each event.
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 |
61 | Tom Select will work on RTL websites if the dir attribute is set for the context of your Tom Select instance. 62 |
dir
Controls can be locked to prevent user interaction.
The options are created from an array in JavaScript.
Images can be added to option and item elements with custom render templates and data-* attributes
This shows how it performs with 15,000 items.
12 | Order matters sometimes. Use the ← and → arrow keys to move the caret between items. 13 |
No additional configuration settings for this plugin
Listen for 'change' events on the original input/select and update Tom Select accordingly
A callback that returns an html string used to create the button
The value of the title attribute on the close button
The CSS class name of the close button
Drag 'n drop selected items. Requires jQuery + jQuery UI
An html string used to generate the header
The text of the header
The CSS class name of the header
The CSS class name of the dropdown header title
The CSS class name of the dropdown header label row
The CSS class name of the dropdown header close button
18 | The input_autogrow plugin will increase the width of the input as users type. 19 |
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 |
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 |
56 | For single selects, you may prefer the Clear Button plugin 57 |
The text that will be displayed in each button
The value of the title attribute on each button
The CSS class name of each button
A function taking option data as an argument and returning a text string that will be assigned to the control input value
firstUrl(query_string)
setNextUrl(query_string,next_url)
getUrl(query_string)