@danielgindi/selectbox 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/DropList.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  import DomEventsSink from '@danielgindi/dom-utils/lib/DomEventsSink';
19
19
  import VirtualListHelper from '@danielgindi/virtual-list-helper';
20
20
  import {
21
+ VALUE_BACK_SPACE,
21
22
  VALUE_DOWN,
22
23
  VALUE_END,
23
24
  VALUE_ENTER, VALUE_ESCAPE,
@@ -62,6 +63,7 @@ const hasOwnProperty = Object.prototype.hasOwnProperty;
62
63
  * @property {function(item: DropList.ItemBase, itemEl: Element):(*|false)} [renderNoResultsItem]
63
64
  * @property {function(item: DropList.ItemBase, itemEl: Element)} [unrenderNoResultsItem]
64
65
  * @property {function(name: string, data: *)} [on]
66
+ * @property {boolean} [isHeaderVisible=false] show header element
65
67
  * @property {boolean} [searchable=false] include inline search box
66
68
  * @property {string} [noResultsText='No matching results'] text for no results (empty for none)
67
69
  * @property {number} [filterThrottleWindow=300] throttle time (milliseconds) for filtering
@@ -134,6 +136,7 @@ let defaultOptions = {
134
136
 
135
137
  on: null,
136
138
 
139
+ isHeaderVisible: false,
137
140
  searchable: false,
138
141
  noResultsText: 'No matching results',
139
142
  filterThrottleWindow: 300,
@@ -206,6 +209,7 @@ class DropList {
206
209
  positionOptionsProvider: o.positionOptionsProvider ?? null,
207
210
 
208
211
  searchable: o.searchable,
212
+ isHeaderVisible: o.isHeaderVisible,
209
213
 
210
214
  silenceEvents: true,
211
215
  mitt: mitt(),
@@ -253,33 +257,24 @@ class DropList {
253
257
  });
254
258
  }
255
259
 
260
+ p.el = wrapperEl;
261
+
256
262
  let menuEl = createElement('ul');
257
263
  menuEl.role = 'menu';
264
+ p.menuEl = menuEl;
258
265
 
259
- if (o.searchable) {
260
- p.headerEl = createElement('div', {
261
- class: p.baseClassName + '_header',
262
- });
263
-
264
- p.searchInput = createElement('input', {
265
- type: 'search',
266
- role: 'searchbox',
267
- tabindex: '0',
268
- autocorrect: 'off',
269
- autocomplete: 'off',
270
- autocapitalize: 'off',
271
- spellcheck: 'false',
272
- 'aria-autocomplete': 'list',
273
- });
266
+ p.headerEl = createElement('div', {
267
+ class: p.baseClassName + '_header',
268
+ });
274
269
 
275
- p.headerEl.appendChild(p.searchInput);
276
- wrapperEl.appendChild(p.headerEl);
270
+ if (o.searchable) {
271
+ this.setSearchable(true);
277
272
  }
278
273
 
279
- wrapperEl.appendChild(menuEl);
274
+ if (o.isHeaderVisible)
275
+ this.setHeaderVisible(o.isHeaderVisible);
280
276
 
281
- p.el = wrapperEl;
282
- p.menuEl = menuEl;
277
+ wrapperEl.appendChild(menuEl);
283
278
 
284
279
  p.items = [];
285
280
  p.groupCount = 0; // This will keep state of how many `group` items we have
@@ -1066,6 +1061,35 @@ class DropList {
1066
1061
  return items[index]?.[ItemSymbol];
1067
1062
  }
1068
1063
 
1064
+ /**
1065
+ * Return the item element at the given original index, if it exists.
1066
+ * The item may not be currently rendered, and null will be returned
1067
+ * @param index
1068
+ * @returns {HTMLElement|null}
1069
+ */
1070
+ itemElementAtIndex(index) {
1071
+ const p = this._p;
1072
+ if (!p.filteredItems)
1073
+ return this.filteredElementItemAtIndex(index);
1074
+
1075
+ index = p.filteredItems.indexOf(this._p.items[index]);
1076
+
1077
+ const li = p.virtualListHelper.getItemElementAt(index);
1078
+ return li ?? null;
1079
+ }
1080
+
1081
+ /**
1082
+ * Return the item element at the given index, if it exists.
1083
+ * The item may not be currently rendered, and null will be returned
1084
+ * @param index
1085
+ * @returns {HTMLElement|null}
1086
+ */
1087
+ filteredElementItemAtIndex(index) {
1088
+ const p = this._p;
1089
+ const li = p.virtualListHelper.getItemElementAt(index);
1090
+ return li ?? null;
1091
+ }
1092
+
1069
1093
  /**
1070
1094
  * @param {function(dropList: DropList):DropList.PositionOptions} fn
1071
1095
  * @returns {DropList}
@@ -1131,7 +1155,30 @@ class DropList {
1131
1155
  let filteredItems;
1132
1156
 
1133
1157
  if (typeof fn === 'function') {
1134
- filteredItems = p.filterFn(p.items, term);
1158
+ // Send the original items to the filter function
1159
+ filteredItems = p.filterFn(
1160
+ p.items.map(x => x[ItemSymbol] ?? x),
1161
+ term);
1162
+
1163
+ if (Array.isArray(filteredItems)) {
1164
+ // And back
1165
+ filteredItems = filteredItems.map(oitem => {
1166
+ let our = oitem[ItemSymbol];
1167
+ if (!our) {
1168
+ our = {
1169
+ [ItemSymbol]: oitem,
1170
+ label: oitem[p.labelProp],
1171
+ value: oitem[p.valueProp],
1172
+ _nocheck: !!oitem._nocheck,
1173
+ _nointeraction: !!oitem._nointeraction,
1174
+ _subitems: oitem._subitems,
1175
+ _group: !!oitem._group,
1176
+ _checked: !!oitem._checked,
1177
+ };
1178
+ }
1179
+ return our;
1180
+ });
1181
+ }
1135
1182
  }
1136
1183
 
1137
1184
  // If there was no filter function, or it gave up on filtering.
@@ -1889,6 +1936,72 @@ class DropList {
1889
1936
  return p.el.parentNode && getComputedStyle(p.el).display !== 'none';
1890
1937
  }
1891
1938
 
1939
+ /**
1940
+ * Change visibility of the header element
1941
+ * You should probably call `relayout()` after this.
1942
+ * @param {boolean} visible
1943
+ */
1944
+ setHeaderVisible(visible) {
1945
+ let isVisible = this.isHeaderVisible();
1946
+ if (isVisible === !!visible)
1947
+ return;
1948
+
1949
+ if (visible) {
1950
+ this._p.el.insertBefore(this._p.headerEl, this._p.el.firstChild ?? null);
1951
+ } else {
1952
+ this._p.headerEl.remove();
1953
+ }
1954
+ }
1955
+
1956
+ /**
1957
+ * Is the header element visible?
1958
+ * @returns {boolean}
1959
+ */
1960
+ isHeaderVisible() {
1961
+ return !!this._p.headerEl.parentNode;
1962
+ }
1963
+
1964
+ /**
1965
+ * Get a reference to the header element in order to add custom content.
1966
+ * @returns {Element}
1967
+ */
1968
+ getHeaderElement() {
1969
+ return this._p.headerEl;
1970
+ }
1971
+
1972
+ /**
1973
+ * Set inline search visibility
1974
+ * You should probably call `relayout()` after this.
1975
+ * @param {boolean} searchable
1976
+ */
1977
+ setSearchable(searchable) {
1978
+ const p = this._p;
1979
+
1980
+ if (!!p.searchInput === !!searchable)
1981
+ return;
1982
+
1983
+ if (searchable) {
1984
+ p.searchInput = createElement('input', {
1985
+ type: 'search',
1986
+ role: 'searchbox',
1987
+ tabindex: '0',
1988
+ autocorrect: 'off',
1989
+ autocomplete: 'off',
1990
+ autocapitalize: 'off',
1991
+ spellcheck: 'false',
1992
+ 'aria-autocomplete': 'list',
1993
+ });
1994
+
1995
+ p.headerEl.appendChild(p.searchInput);
1996
+ } else {
1997
+ if (p.searchInput.parentNode)
1998
+ p.searchInput.remove();
1999
+ p.searchInput = null;
2000
+ }
2001
+
2002
+ this.setHeaderVisible(searchable);
2003
+ }
2004
+
1892
2005
  hasFocusedItem() {
1893
2006
  return this._p.focusItemIndex > -1;
1894
2007
  }
@@ -2174,7 +2287,7 @@ class DropList {
2174
2287
  const p = this._p;
2175
2288
 
2176
2289
  if (this._hasScroll()) {
2177
- const el = p.el, scrollTop = el.scrollTop;
2290
+ const menuEl = p.menuEl, scrollTop = menuEl.scrollTop;
2178
2291
 
2179
2292
  let itemPos, previousPos = -1;
2180
2293
  let maxIterations = 30; // Some zoom/scroll issues can make it so that it takes almost forever
@@ -2189,12 +2302,12 @@ class DropList {
2189
2302
 
2190
2303
  let itemSize = p.virtualListHelper.getItemSize(itemIndex);
2191
2304
 
2192
- let listHeight = el.clientHeight;
2305
+ let listHeight = menuEl.clientHeight;
2193
2306
 
2194
2307
  if (itemPos < scrollTop) {
2195
- el.scrollTop = itemPos;
2308
+ menuEl.scrollTop = itemPos;
2196
2309
  } else if (itemPos + itemSize > scrollTop + listHeight) {
2197
- el.scrollTop = itemPos + itemSize - listHeight;
2310
+ menuEl.scrollTop = itemPos + itemSize - listHeight;
2198
2311
  }
2199
2312
 
2200
2313
  // force update items, until the positions and sizes are final
@@ -2479,6 +2592,8 @@ class DropList {
2479
2592
  case VALUE_END:
2480
2593
  case VALUE_UP:
2481
2594
  case VALUE_DOWN:
2595
+ p.lastKeyWasChar = false;
2596
+
2482
2597
  event.preventDefault();
2483
2598
 
2484
2599
  switch (event.key) {
@@ -2505,6 +2620,7 @@ class DropList {
2505
2620
 
2506
2621
  case VALUE_LEFT:
2507
2622
  case VALUE_RIGHT:
2623
+ p.lastKeyWasChar = false;
2508
2624
  if (event.key === VALUE_RIGHT && getComputedStyle(event.target).direction !== 'rtl' ||
2509
2625
  event.key === VALUE_LEFT && getComputedStyle(event.target).direction === 'rtl') {
2510
2626
  const items = p.filteredItems ?? p.items;
@@ -2522,22 +2638,31 @@ class DropList {
2522
2638
  break;
2523
2639
 
2524
2640
  case VALUE_ENTER:
2641
+ p.lastKeyWasChar = false;
2525
2642
  this.triggerItemSelection(null, event);
2526
2643
  event.preventDefault();
2527
2644
  break;
2528
2645
 
2529
2646
  case VALUE_SPACE:
2647
+ if (event.target.tagName === 'BUTTON' ||
2648
+ event.target.tagName === 'INPUT' && p.lastKeyWasChar) return;
2649
+
2530
2650
  this.toggleFocusedItem();
2531
2651
  event.preventDefault();
2532
2652
  break;
2533
2653
 
2534
2654
  case VALUE_ESCAPE:
2655
+ p.lastKeyWasChar = false;
2535
2656
  event.preventDefault();
2536
2657
  this.hide();
2537
2658
  break;
2538
2659
 
2539
2660
  default: {
2540
- if (event.type === 'keydown') return;
2661
+ if (event.target.tagName === 'INPUT' ||
2662
+ event.target.tagName === 'TEXTAREA') {
2663
+ const character = event.key || String.fromCharCode(event.keyCode);
2664
+ p.lastKeyWasChar = character.length === 1 || event.key === VALUE_BACK_SPACE;
2665
+ }
2541
2666
 
2542
2667
  // Inline search box not available, then support typing to focus by first letters
2543
2668
  if (!p.searchable)
@@ -2552,8 +2677,10 @@ class DropList {
2552
2677
  const p = this._p;
2553
2678
 
2554
2679
  // noinspection JSDeprecatedSymbols
2555
- let character = evt.key || String.fromCharCode(evt.keyCode);
2556
- if (character.length !== 1) return;
2680
+ const character = evt.key || String.fromCharCode(evt.keyCode);
2681
+ const isChar = character.length === 1;
2682
+ p.lastKeyWasChar = isChar || evt.key === VALUE_BACK_SPACE;
2683
+ if (!isChar) return;
2557
2684
 
2558
2685
  clearTimeout(p.filterTimer);
2559
2686
 
@@ -3143,3 +3270,4 @@ class DropList {
3143
3270
  }
3144
3271
 
3145
3272
  export default DropList;
3273
+ export { ItemSymbol };
package/lib/SelectBox.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  } from '@danielgindi/dom-utils/lib/DomCompat';
23
23
 
24
24
  import DomEventsSink from '@danielgindi/dom-utils/lib/DomEventsSink';
25
- import DropList from './DropList';
25
+ import DropList, { ItemSymbol } from './DropList';
26
26
  import {
27
27
  VALUE_BACK_SPACE,
28
28
  VALUE_DELETE, VALUE_DOWN, VALUE_END, VALUE_ENTER,
@@ -35,7 +35,6 @@ import {
35
35
  } from 'keycode-js';
36
36
  import mitt from 'mitt';
37
37
 
38
- const ItemSymbol = Symbol('item');
39
38
  const DestroyedSymbol = Symbol('destroyed');
40
39
  const RestMultiItemsSymbol = Symbol('rest_multi_items');
41
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danielgindi/selectbox",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "A collection of dom utilities. So you can work natively with the dom without dom frameworks.",
5
5
  "main": "dist/lib.cjs.min.js",
6
6
  "module": "lib/index.js",
@@ -51,7 +51,7 @@
51
51
  "sass": "^1.85.0"
52
52
  },
53
53
  "dependencies": {
54
- "@danielgindi/dom-utils": "^1.0.10",
54
+ "@danielgindi/dom-utils": "^1.0.11",
55
55
  "@danielgindi/virtual-list-helper": "^1.0.13",
56
56
  "fast-deep-equal": "^3.1.3",
57
57
  "keycode-js": "^3.1.0",
package/vue/DropList.vue CHANGED
@@ -53,6 +53,10 @@ export default {
53
53
  type: Boolean,
54
54
  default: false,
55
55
  },
56
+ isHeaderVisible: {
57
+ type: Boolean,
58
+ default: false,
59
+ },
56
60
  searchable: {
57
61
  type: Boolean,
58
62
  default: false,
@@ -204,6 +208,7 @@ export default {
204
208
  for (let key of ['autoItemBlur', 'capturesFocus', 'multi',
205
209
  'autoCheckGroupChildren', 'useExactTargetWidth', 'constrainToWindow',
206
210
  'autoFlipDirection', 'estimateWidth',
211
+ 'isHeaderVisible',
207
212
  'searchable', 'filterOnEmptyTerm',
208
213
  'filterGroups', 'filterEmptyGroups']) {
209
214
  if (typeof this[key] === 'boolean') {
@@ -340,8 +345,16 @@ export default {
340
345
  this._recreateList();
341
346
  },
342
347
 
343
- searchable() {
344
- this._recreateList();
348
+ searchable(v) {
349
+ if (this.nonReactive.instance)
350
+ this.nonReactive.instance.setSearchable(v);
351
+ this.relayout();
352
+ },
353
+
354
+ isHeaderVisible(v) {
355
+ if (this.nonReactive.instance)
356
+ this.nonReactive.instance.setHeaderVisible(v);
357
+ this.relayout();
345
358
  },
346
359
 
347
360
  positionOptions: {
@@ -437,6 +450,11 @@ export default {
437
450
  list.setSingleSelectedItemByValue(modelValue === null ? undefined : modelValue);
438
451
  }
439
452
 
453
+ const headerRenderer = createSlotBasedRenderFunc(this, 'header');
454
+ if (headerRenderer) {
455
+ headerRenderer({}, list.getHeaderElement());
456
+ }
457
+
440
458
  list.show();
441
459
 
442
460
  this._setupAutoRelayout();
@@ -506,6 +524,11 @@ export default {
506
524
  this.nonReactive.instance.relayout();
507
525
  },
508
526
 
527
+ getHeaderElement() {
528
+ if (this.nonReactive.instance)
529
+ this.nonReactive.instance.getHeaderElement();
530
+ },
531
+
509
532
  elContains(other, considerSublists = true) {
510
533
  return !!this.listRef?.elContains(other, considerSublists);
511
534
  },