select

Better select widgets in vanilla javascript.  https://p.ce9e.org/select/demo/
git clone https://git.ce9e.org/select.git

commit
a23b7af67a09d7320cc69a8df9b41cecd8aaafc3
parent
f58e4f62abddd65707c39fcedb2ff22f13498c7a
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2023-08-08 11:48
Merge pull request #2 from xi/feature-inline-multi

Display values inside of input

Diffstat

M package.json 1 +
M select.css 19 ++++++++++++++++++-
M select.js 24 +++++++-----------------
M tags.js 30 +++++++++---------------------
A values.js 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

5 files changed, 117 insertions, 39 deletions


diff --git a/package.json b/package.json

@@ -13,6 +13,7 @@
   13    13   "files": [
   14    14     "tags.js",
   15    15     "select.js",
   -1    16     "values.js",
   16    17     "utils.js",
   17    18     "select.css",
   18    19     "README.md",

diff --git a/select.css b/select.css

@@ -9,7 +9,7 @@
    9     9 
   10    10 .select__dropdown {
   11    11 	position: absolute;
   12    -1 	z-index: 1;
   -1    12 	z-index: 2;
   13    13 	list-style: none;
   14    14 	margin: 0;
   15    15 	padding: 0;
@@ -64,13 +64,23 @@
   64    64 	padding-left: 2em;
   65    65 }
   66    66 
   -1    67 .select__input {
   -1    68 	position: relative;
   -1    69 }
   -1    70 .select__input input {
   -1    71 	position: relative;  /* on top of values */
   -1    72 }
   -1    73 
   67    74 .select__values {
   -1    75 	position: absolute;
   68    76 	display: inline;
   69    77 	padding: 0;
   70    78 	margin: 0;
   71    79 }
   72    80 .select__values li {
   73    81 	display: inline-block;
   -1    82 	position: relative;
   -1    83 	z-index: 1;  /* on top of input */
   74    84 	margin: 0.1em 0.2em;
   75    85 	margin-inline-start: 0;
   76    86 	cursor: default;
@@ -82,3 +92,10 @@
   82    92 	border: 1px solid ThreeDShadow;
   83    93 	border-radius: 0.3em;
   84    94 }
   -1    95 
   -1    96 .select__measure {
   -1    97 	width: 0;
   -1    98 	height: 0;
   -1    99 	overflow: hidden;
   -1   100 	white-space: pre;
   -1   101 }

diff --git a/select.js b/select.js

@@ -1,4 +1,5 @@
    1     1 import { KEYS, randomString, create } from './utils.js';
   -1     2 import { Values } from './values.js';
    2     3 
    3     4 export class Select {
    4     5 	constructor(id, original) {
@@ -11,6 +12,8 @@ export class Select {
   11    12 		this.createElements();
   12    13 		original.hidden = true;
   13    14 		original.before(this.wrapper);
   -1    15 
   -1    16 		this.updateValue();
   14    17 	}
   15    18 
   16    19 	createElements() {
@@ -20,9 +23,8 @@ export class Select {
   20    23 
   21    24 		if (this.original.multiple) {
   22    25 			var inputWrapper = create('<div class="select__input">');
   23    -1 			this.values = create('<ul class="select__values" aria-live="polite">')
   24    -1 			inputWrapper.append(this.values);
   25    26 			inputWrapper.append(this.input);
   -1    27 			this.values = new Values(this.input, this.original.dataset.selectValueClass);
   26    28 			this.wrapper.append(inputWrapper);
   27    29 		} else {
   28    30 			this.wrapper.append(this.input);
@@ -58,8 +60,6 @@ export class Select {
   58    60 		this.dropdown.onmousedown = event => {
   59    61 			event.preventDefault();
   60    62 		};
   61    -1 
   62    -1 		this.updateValue();
   63    63 	}
   64    64 
   65    65 	isMatch(s) {
@@ -90,19 +90,9 @@ export class Select {
   90    90 			this.input.value = '';
   91    91 			this.inputDirty = false;
   92    92 			this.updateValidity();
   93    -1 			this.values.innerHTML = '';
   94    -1 			Array.from(this.original.options).forEach(op => {
   95    -1 				if (op.selected && op.label) {
   96    -1 					var li = document.createElement('li');
   97    -1 					li.textContent = op.label;
   98    -1 					li.className = this.original.dataset.selectValueClass || 'select__value';
   99    -1 					li.onclick = () => {
  100    -1 						op.selected = false;
  101    -1 						this.updateValue();
  102    -1 						this.input.focus();
  103    -1 					};
  104    -1 					this.values.append(li);
  105    -1 				}
   -1    93 			this.values.update(this.original, () => {
   -1    94 				this.updateValue();
   -1    95 				this.input.focus();
  106    96 			});
  107    97 		} else {
  108    98 			if (this.original.selectedOptions.length) {

diff --git a/tags.js b/tags.js

@@ -1,4 +1,5 @@
    1     1 import { KEYS, randomString, create } from './utils.js';
   -1     2 import { Values } from './values.js';
    2     3 
    3     4 export class TagInput {
    4     5 	constructor(id, original) {
@@ -9,18 +10,19 @@ export class TagInput {
    9    10 		this.createElements();
   10    11 		original.hidden = true;
   11    12 		original.before(this.wrapper);
   -1    13 
   -1    14 		this.updateValue();
   12    15 	}
   13    16 
   14    17 	createElements() {
   15    -1 		this.wrapper = document.createElement('div');
   16    -1 
   17    -1 		this.values = create('<ul class="select__values" aria-live="polite">');
   18    -1 		this.wrapper.append(this.values);
   -1    18 		this.wrapper = create('<div class="select__input">');
   19    19 
   20    20 		this.input = document.createElement('input');
   21    21 		this.input.className = this.original.dataset.tagsInputClass || '';
   22    22 		this.wrapper.append(this.input);
   23    23 
   -1    24 		this.values = new Values(this.input, this.original.dataset.tagsValueClass);
   -1    25 
   24    26 		this.datalist = document.createElement('datalist');
   25    27 		this.datalist.innerHTML = this.original.innerHTML;
   26    28 		this.datalist.id = this.id + '-list';
@@ -39,27 +41,13 @@ export class TagInput {
   39    41 		this.original.onchange = () => {
   40    42 			this.input.setCustomValidity(this.original.validationMessage);
   41    43 		};
   42    -1 
   43    -1 		this.updateValue();
   44    44 	}
   45    45 
   46    46 	updateValue() {
   47    47 		this.input.value = '';
   48    -1 		this.values.innerHTML = '';
   49    -1 		Array.from(this.original.options).forEach(op => {
   50    -1 			if (op.selected && op.label) {
   51    -1 				var li = document.createElement('li');
   52    -1 				li.textContent = op.label;
   53    -1 				li.className = this.original.dataset.tagsValueClass || 'select__value';
   54    -1 				li.onclick = () => {
   55    -1 					op.selected = false;
   56    -1 					this.updateValue();
   57    -1 					this.input.focus();
   58    -1 				};
   59    -1 				this.values.append(li);
   60    -1 			} else if (!op.selected && op.hasAttribute('data-tag-custom')) {
   61    -1 				op.remove();
   62    -1 			}
   -1    48 		this.values.update(this.original, () => {
   -1    49 			this.updateValue();
   -1    50 			this.input.focus();
   63    51 		});
   64    52 	}
   65    53 

diff --git a/values.js b/values.js

@@ -0,0 +1,82 @@
   -1     1 import { create } from './utils.js';
   -1     2 
   -1     3 var createMeasure = function(target) {
   -1     4 	var wrapper = create('<div class="select__measure" aria-hidden="true">');
   -1     5 	var span = document.createElement('span');
   -1     6 	target.after(wrapper);
   -1     7 	wrapper.append(span);
   -1     8 
   -1     9 	return text => {
   -1    10 		span.textContent = text;
   -1    11 		var rect = span.getBoundingClientRect();
   -1    12 		return rect.width;
   -1    13 	};
   -1    14 };
   -1    15 
   -1    16 export class Values {
   -1    17 	constructor(input, valueClass) {
   -1    18 		this.gap = 4;
   -1    19 		this.input = input;
   -1    20 		this.valueClass = valueClass || 'select__value';
   -1    21 
   -1    22 		this.measure = createMeasure(input);
   -1    23 		this.el = create('<ul class="select__values" aria-live="polite">');
   -1    24 		input.before(this.el);
   -1    25 
   -1    26 		input.addEventListener('input', this.updateSize.bind(this));
   -1    27 		window.addEventListener('resize', this.updateSize.bind(this));
   -1    28 	}
   -1    29 
   -1    30 	updateSize() {
   -1    31 		var style = getComputedStyle(this.input);
   -1    32 
   -1    33 		// We may already have changed paddingTop, so we assume that original
   -1    34 		// paddingTop and paddingBottom are the same
   -1    35 		var paddingTop = parseInt(style.paddingBottom, 10);
   -1    36 
   -1    37 		this.el.style.top = paddingTop + 'px';
   -1    38 		this.el.style.bottom = style.paddingBottom;
   -1    39 		this.el.style.left = style.paddingLeft;
   -1    40 		this.el.style.right = style.paddingRight;
   -1    41 
   -1    42 		var n = this.el.children.length;
   -1    43 		if (n > 0) {
   -1    44 			var first = this.el.children[0].getBoundingClientRect();
   -1    45 			var last = this.el.children[n - 1].getBoundingClientRect();
   -1    46 			var height = last.top - first.top;
   -1    47 			var width = style.direction === 'ltr'
   -1    48 				? last.right - first.left
   -1    49 				: first.right - last.left;
   -1    50 
   -1    51 			if (width + this.gap + this.measure(this.input.value) < this.el.clientWidth) {
   -1    52 				this.input.style.paddingTop = `${paddingTop + height}px`;
   -1    53 				this.input.style.textIndent = `${width + this.gap}px`;
   -1    54 			} else {
   -1    55 				this.input.style.paddingTop = `${paddingTop + height + last.height + this.gap}px`;
   -1    56 				this.input.style.textIndent = '0';
   -1    57 			}
   -1    58 		} else {
   -1    59 			this.input.style.paddingTop = `${paddingTop}px`;
   -1    60 			this.input.style.textIndent = '0';
   -1    61 		}
   -1    62 	}
   -1    63 
   -1    64 	update(original, onChange) {
   -1    65 		this.el.innerHTML = '';
   -1    66 		Array.from(original.options).forEach(op => {
   -1    67 			if (op.selected && op.label) {
   -1    68 				var li = document.createElement('li');
   -1    69 				li.textContent = op.label;
   -1    70 				li.className = this.valueClass;
   -1    71 				li.onclick = () => {
   -1    72 					op.selected = false;
   -1    73 					onChange();
   -1    74 				};
   -1    75 				this.el.append(li);
   -1    76 			} else if (!op.selected && op.hasAttribute('data-tag-custom')) {
   -1    77 				op.remove();
   -1    78 			}
   -1    79 		});
   -1    80 		this.updateSize();
   -1    81 	}
   -1    82 }