extended-listbox is a vanilla JavaScript/TypeScript UI component (v6.0.0) that serves as a customizable alternative to the native HTML <select> element. It renders a div-based list structure, supporting single-select and multi-select modes, grouped/hierarchical items, a search bar with filtering, keyboard navigation (arrow keys, Enter), and a full programmatic API for item manipulation. No jQuery or other runtime dependencies are required. Published to npm as extended-listbox.
- Author: Christian Kotzbauer
- License: MIT
- Repository:
github.com/ckotzbauer/extended-listbox - npm:
https://www.npmjs.com/package/extended-listbox
| Category | Technology |
|---|---|
| Language | TypeScript 5.9, targeting ES2015 |
| Bundler | Rollup 4.59 with @rollup/plugin-typescript |
| Minification | Terser 5.46 |
| Styles | SCSS (Sass 1.97), PostCSS with Autoprefixer |
| Testing | Jest 29.7 with ts-jest and jest-environment-jsdom |
| Linting | ESLint 8.57 with @typescript-eslint/parser + Prettier |
| Formatting | Prettier 3.8 |
| Task Runner | npm scripts, npm-run-all2 for sequential/parallel scripts |
| Build Helpers | ts-node (executes build scripts), rimraf, ncp |
| Dependency Mgmt | Renovate (extends ckotzbauer/renovate-config) |
extended-listbox/
├── build/ # Build orchestration scripts (TypeScript, run via ts-node)
│ ├── build.ts # Main build: Rollup bundle, Terser minify, Sass compile, copy SCSS
│ ├── build-post.ts # Post-build: moves .d.ts files into dist/types/, copies typings.d.ts
│ └── rollup.ts # Rollup config: UMD output, TypeScript plugin
├── dist/ # Build output (committed)
│ ├── extended-listbox.js # UMD bundle (unminified)
│ ├── extended-listbox.min.js # UMD bundle (minified)
│ ├── extended-listbox.css # Compiled CSS (expanded)
│ ├── extended-listbox.min.css # Compiled CSS (compressed)
│ ├── scss/ # Raw SCSS source copy for consumers
│ ├── types/ # TypeScript declaration files
│ └── typings.d.ts # Main type declaration entry
├── src/
│ ├── index.ts # Entry point: `listBox()` factory function (default export)
│ ├── factory.ts # `createListBox()` - merges config defaults, instantiates correct class
│ ├── base-list-box.ts # Abstract base class with all shared DOM logic and public API
│ ├── single-select-list-box.ts # SingleSelectListBox - single selection behavior
│ ├── multi-select-list-box.ts # MultiSelectListBox - multi selection with Ctrl+click
│ ├── test-utils.ts # Shared test helpers (createInstance, click simulation, assertions)
│ ├── typings.d.ts # Public type re-exports and ListBoxFn declaration
│ ├── tsconfig.json # Source-specific tsconfig (ES2015 modules, baseUrl: "./")
│ ├── styles/
│ │ └── extended-listbox.scss # Component styles with configurable SCSS variables
│ ├── types/
│ │ ├── options.ts # ListBoxOptions interface and defaults
│ │ ├── list-box-item.ts # ListBoxItem interface
│ │ ├── list-box-event.ts # ListBoxEvent interface
│ │ ├── instance.ts # Instance<K> interface (public API), ListBoxNameMap, ListBoxFn
│ │ └── globals.ts # Global type augmentations (HTMLElement.listBox, etc.)
│ └── __tests__/
│ ├── base-list-box.spec.ts # Tests for shared functionality (items, searchbar, events, API)
│ ├── single-select-list-box.spec.ts # Single-select behavior tests
│ └── multi-select-list-box.spec.ts # Multi-select behavior tests (click, Ctrl+click)
├── doc/pages/ # Documentation pages (for gh-pages deployment)
├── coverage/ # Jest coverage output (committed)
├── .github/workflows/
│ ├── main.yml # CI: build + lint + test on every push
│ ├── release.yml # Release: manual dispatch, npm publish, docs deploy
│ └── update-snyk.yml # Weekly Snyk security monitoring
├── package.json
├── tsconfig.json # Root tsconfig (CommonJS, baseUrl: "src")
├── tsconfig.declarations.json # Declarations-only build (emits typings.d.ts to dist/)
├── tsconfig.typecheck.json # Type-checking config (includes all src/**/*.ts)
├── tsconfig.test.json # (Currently unused - tests use jest.json ts-jest config)
├── jest.json # Jest configuration
├── .eslintrc.js # ESLint configuration
├── .prettierrc.js # Prettier configuration
├── .editorconfig # Editor settings
├── renovate.json # Renovate bot config
├── deploy-docs.sh # Legacy Travis-based docs deployment script
└── .npmignore # Excludes build/, src/, config files from npm package
The component uses a template method / abstract class pattern:
-
BaseListBox<K>(src/base-list-box.ts) - Abstract generic class parameterized byK extends keyof ListBoxNameMap(either"single"or"multi"). Contains all shared logic:- DOM creation (
_createListbox,_createSearchbar,_createList) - Item management (add, remove, move, locate)
- Search/filter bar with live filtering
- Keyboard navigation (arrow up/down, Enter)
- Event system (
_fireEventdispatches to option callbacks) - Public API methods:
addItem,addItems,removeItem,removeItems,destroy,clearSelection,getItem,getItems,moveItemUp,moveItemDown,moveItemToTop,moveItemToBottom,enable,getSelection
- DOM creation (
-
SingleSelectListBox(src/single-select-list-box.ts) - ExtendsBaseListBox<"single">. Selection isListBoxItem | null. Clicking any item deselects the previous one. Filtering auto-selects the first visible item. -
MultiSelectListBox(src/multi-select-list-box.ts) - ExtendsBaseListBox<"multi">. Selection isListBoxItem[]. Supports Ctrl+click for additive selection; plain click replaces selection.
src/index.ts exports the listBox function as the default export. It accepts a CSS selector string, a DOM Node, or a NodeList, plus a mode ("single" or "multi") and optional configuration. Internally calls createListBox() from src/factory.ts.
ListBoxNameMapmaps mode strings to selection types:single -> ListBoxItem | null,multi -> ListBoxItem[]Instance<K>is the public API interface, generic over the modeListBoxFnprovides overloaded call signatures for the entry pointglobals.tsaugmentsHTMLElement,NodeList,HTMLCollectionwith alistBoxmethod
Custom callback-based events (not DOM CustomEvents). Options like onValueChanged, onFilterChanged, onItemsChanged, onItemEnterPressed, onItemDoubleClicked are called directly when the corresponding action occurs. Events receive a ListBoxEvent object with eventName, target, and args.
SCSS with configurable variables (prefixed $listbox-). All CSS classes use a listbox- namespace. Key classes defined as static constants on BaseListBox:
.listbox-root,.listbox,.listbox-item,.listbox-item-selected,.listbox-item-disabled,.listbox-item-group,.listbox-item-child,.listbox-searchbar,.listbox-searchbar-button,.listbox-disabled
Default dimensions: 170px wide, 300px tall, 5px border-radius.
Runs four sequential steps via npm-run-all2:
build:pre-rimraf dist(clean output)build:build-ts-node --transpile-only build/build.ts:- Rollup bundles
src/index.tstodist/extended-listbox.js(UMD format,exports: "default") - Terser minifies to
dist/extended-listbox.min.js - Sass compiles SCSS to
dist/extended-listbox.cssanddist/extended-listbox.min.css - Copies raw SCSS to
dist/scss/
- Rollup bundles
build:types-tsc -p tsconfig.declarations.json(emits only.d.tsfiles fromsrc/typings.d.ts)build:post-ts-node --transpile-only build/build-post.ts(moves.d.tsfiles intodist/types/, copiessrc/typings.d.tstodist/typings.d.ts)
UMD bundle with a default export. Banner includes version and MIT license comment. The dist/ directory is committed to the repository.
Defined in package.json "files": dist/, doc/pages/, CHANGELOG.md, doc/ROADMAP.md. The .npmignore additionally excludes build scripts, source, and config files.
main:dist/js/extended-listbox.jstypings:dist/extended-listbox.d.ts
Jest 29.7 with ts-jest for TypeScript transpilation and jest-environment-jsdom for DOM simulation.
{
"preset": "ts-jest",
"testEnvironment": "jsdom",
"moduleDirectories": ["node_modules", "src"],
"transform": { ".ts": ["ts-jest", { "isolatedModules": true }] }
}Three spec files in src/__tests__/:
base-list-box.spec.ts(42 tests) - Root/list CSS classes, searchbar creation, watermark, button with icon and callback, item rendering (simple/disabled/selected/header/with-id/with-children), API methods (addItem, addItems, removeItem, removeItems, destroy, clearSelection, getItem, getItems, moveItemUp, moveItemDown, moveItemToBottom, moveItemToTop, enable/disable, getSelection), keyboard events (Enter, ArrowUp, ArrowDown), custom events (valueChanged, itemsChanged, filterChanged, itemDoubleClicked)single-select-list-box.spec.ts(7 tests) - Construction, searchbar, default values, click behavior (single selection only), onValueChanged callbackmulti-select-list-box.spec.ts(11 tests) - Construction, searchbar, default values, click/Ctrl+click behavior, multi-selection scenarios, onValueChanged callback
Shared helpers:
createInstance(mode, options?, items?)- Creates a DOM fixture and instantiates a listboxbeforeEachTest()/afterEachTest()- Manages a#fixturediv ondocument.bodyclick(element, ctrl?)- Simulates click events with optional Ctrl keychild(element, index?)/children(element)- DOM child accessorselementEquals()/itemEquals()- Assertion helpers comparing data items to DOM elements
- Parser:
@typescript-eslint/parser(ECMAScript 2018, ES modules) - Extends:
plugin:@typescript-eslint/recommended,prettier - Key rules:
@typescript-eslint/naming-convention: enforces camelCase for variables- Disabled:
no-use-before-define,explicit-function-return-type,no-explicit-any,prefer-interface,no-object-literal-type-assertion
- Ignores:
.gitignorepaths,dist/,doc/
trailingComma: "es5"printWidth: 130
- Line endings: LF
- Indent: 4 spaces (2 for JSON)
- Charset: UTF-8 BOM
- Trailing whitespace: trimmed
- Final newline: inserted
strict: trueacross all tsconfig filesnoUnusedLocals: true,noUnusedParameters: truenoImplicitAny: true(root tsconfig)- Target: ES2015
- Libs:
es2015,dom,es2017.object,esnext.asynciterable
- Trigger: Push to any branch
- Reusable workflow:
ckotzbauer/actions-toolkit/.github/workflows/[email protected] - Steps:
npm ci->npm run build->npm run lint->npm test - Coverage: Reports
coverage/lcov.info(likely to Codecov)
- Trigger: Manual
workflow_dispatchwith aversioninput - Reusable workflow:
ckotzbauer/actions-toolkit/.github/workflows/[email protected] - Steps: Build, test, publish to npm, create GitHub release with
dist/as artifact - Secrets:
GITHUB_TOKEN,REPO_ACCESS(PAT),NPM_TOKEN - Post-release: Triggers a separate
deploy-docsworkflow on main
- Trigger: Weekly schedule (Monday 12:00 UTC) or manual dispatch
- Action: Runs
snyk monitorfor vulnerability tracking
Renovate bot configured via renovate.json, extending shared presets from ckotzbauer/renovate-config (default + weekly schedule).
| Command | Description |
|---|---|
npm run build |
Full build: clean, bundle JS (Rollup+Terser), compile SCSS, emit types |
npm test |
Run type-checking (tsc --noEmit) then Jest unit tests with coverage |
npm run test:unit |
Run Jest tests only with coverage |
npm run test:typecheck |
TypeScript type-checking only (no emit) |
npm run lint |
Run ESLint on all .ts files |
npm run format |
Run Prettier on all .ts files (write mode) |
- No runtime dependencies. The package has zero production dependencies; everything is in
devDependencies. - UMD output. The bundle uses UMD format with a default export, consumable via script tag, CommonJS
require, or ES module import. - Dist is committed. The
dist/directory is checked into version control. - Variable naming. ESLint enforces camelCase for all variable-like identifiers via
@typescript-eslint/naming-convention. - CSS class constants. All CSS class names are defined as
public staticstring constants onBaseListBox(e.g.,BaseListBox.LIST_ITEM_CLASS). Always reference these constants rather than using string literals in source code. - Event naming convention. Internal event names are camelCase strings (e.g.,
"valueChanged"). The corresponding option callback is"on"+ PascalCase (e.g.,onValueChanged). The mapping is done dynamically in_fireEvent. - Generic mode typing. The
"single"/"multi"mode string is a generic parameterK extends keyof ListBoxNameMapthreading through the entire type hierarchy, providing type-safe selection return types. - Test isolation. Each test creates a fresh DOM fixture via
beforeEachTest()and tears it down viaafterEachTest(). Tests usejsdom-- there is no browser-based test runner. - SCSS variables are
!default. Consumers can override all style variables (colors, dimensions, border-radius) before importing the SCSS source fromdist/scss/. - Releases are manual. The release workflow requires a manual
workflow_dispatchwith a version string; there is no automated semantic release.