@ekzo-dev/bootstrap-addons 5.2.23 → 5.2.25

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ekzo-dev/bootstrap-addons",
3
3
  "description": "Aurelia Bootstrap additional component",
4
- "version": "5.2.23",
4
+ "version": "5.2.25",
5
5
  "homepage": "https://github.com/ekzo-dev/aurelia-components/tree/main/packages/bootstrap-addons",
6
6
  "repository": {
7
7
  "type": "git",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
- "@ekzo-dev/bootstrap": "^5.2.18",
12
+ "@ekzo-dev/bootstrap": "^5.2.19",
13
13
  "@ekzo-dev/vanilla-jsoneditor": "^0.23.7",
14
14
  "@ekzo-dev/toolkit": "^1.2.4",
15
15
  "@fortawesome/free-solid-svg-icons": "^6.5.2",
@@ -5,11 +5,13 @@ import { valueConverter } from 'aurelia';
5
5
  export class Filter {
6
6
  toView(
7
7
  list: Map<string, ISelectOption[]> | ISelectOption[],
8
- search: string
8
+ search: string,
9
+ emptyOption?: ISelectOption
9
10
  ): Map<string, ISelectOption[]> | ISelectOption[] {
10
- if (search === '') return list;
11
+ if (search === '' && emptyOption === undefined) return list;
11
12
 
12
- const cb = (option: { text: string }) => option.text.toLowerCase().includes(search.toLowerCase());
13
+ const cb = (option: ISelectOption) =>
14
+ option.value !== emptyOption?.value && option.text.toLowerCase().includes(search.toLowerCase());
13
15
 
14
16
  if (list instanceof Map) {
15
17
  const result = new Map<string, ISelectOption[]>();
@@ -28,7 +28,8 @@
28
28
  id.bind="id"
29
29
  class="form-select ${bsSize ? `form-select-${bsSize}` : ''} ${valid ? 'is-valid' : valid === false ? 'is-invalid' : ''}"
30
30
  bs-dropdown-toggle="arrow.bind: false"
31
- value="${selectedOption?.group ? selectedOption.group + ' / ' : ''}${selectedOption?.text ?? ''}"
31
+ value="${valueText}"
32
+ placeholder="${emptyOption?.text ?? ''}"
32
33
  disabled.bind="disabled"
33
34
  required.bind="required"
34
35
  form.bind="form & attr"
@@ -36,7 +37,14 @@
36
37
  title.bind="title & attr"
37
38
  autocomplete="off"
38
39
  keydown.trigger="$event.preventDefault()"
40
+ ref="control"
39
41
  />
42
+ <button
43
+ if.bind="emptyOption && selectedOption?.value !== emptyOption?.value"
44
+ bs-close-button
45
+ type="button"
46
+ click.trigger="selectOption(emptyOption)"
47
+ ></button>
40
48
  <bs-dropdown-menu>
41
49
  <div bs-dropdown-item="text" if.bind="optionsCount > 10">
42
50
  <input class="form-control" placeholder="Filter options" type="search" value.bind="filter & debounce:250" />
@@ -44,19 +52,19 @@
44
52
  <hr bs-dropdown-item="divider" if.bind="optionsCount > 10" />
45
53
  <button
46
54
  type="button"
47
- repeat.for="option of ungroupedOptions | filter:filter"
48
- bs-dropdown-item="active.bind: option.value === selectedOption.value; disabled.bind: option.disabled"
55
+ repeat.for="option of ungroupedOptions | filter:filter:emptyOption"
56
+ bs-dropdown-item="active.bind: option.value === selectedOption?.value; disabled.bind: option.disabled"
49
57
  click.trigger="selectOption(option)"
50
58
  >
51
59
  ${option.text || '&nbsp;'}
52
60
  </button>
53
- <template repeat.for="[k, v] of groupedOptions | filter:filter">
61
+ <template repeat.for="[k, v] of groupedOptions | filter:filter:emptyOption">
54
62
  <h6 bs-dropdown-item="header">${k}</h6>
55
63
  <button
56
64
  class="ps-4"
57
65
  type="button"
58
66
  repeat.for="option of v"
59
- bs-dropdown-item="active.bind: option.value === selectedOption.value; disabled.bind: option.disabled"
67
+ bs-dropdown-item="active.bind: option.value === selectedOption?.value; disabled.bind: option.disabled"
60
68
  click.trigger="selectOption(option)"
61
69
  >
62
70
  ${option.text || '&nbsp;'}
@@ -1,17 +1,40 @@
1
- @import '~bootstrap/scss/functions';
2
- @import '~bootstrap/scss/variables';
3
- @import '~bootstrap/scss/maps';
4
- @import '~bootstrap/scss/mixins';
5
- @import '~bootstrap/scss/forms/form-select';
6
- @import '~bootstrap/scss/forms/form-check';
1
+ @import 'bootstrap/scss/functions';
2
+ @import 'bootstrap/scss/variables';
3
+ @import 'bootstrap/scss/maps';
4
+ @import 'bootstrap/scss/mixins';
5
+ @import 'bootstrap/scss/forms/form-check';
7
6
 
8
7
  bs-select {
9
- display: block;
10
-
11
- > .form-select {
8
+ .form-select {
12
9
  cursor: default;
13
10
  }
14
11
 
12
+ &:has(.btn-close) .form-select {
13
+ padding-right: 4rem;
14
+ }
15
+
16
+ .btn-close {
17
+ display: block;
18
+ position: relative;
19
+ box-sizing: border-box;
20
+ width: 0.8rem;
21
+ height: 0.8rem;
22
+ top: calc(-1px - 1.5rem);
23
+ left: calc(100% - 3.25rem);
24
+ }
25
+
26
+ .form-select-lg + .btn-close {
27
+ top: calc(-1px - 1.8rem);
28
+ }
29
+
30
+ .form-select-sm + .btn-close {
31
+ top: calc(-1px - 1.3rem);
32
+ }
33
+
34
+ &.form-floating .btn-close {
35
+ top: calc(-1px - 2.15rem) !important;
36
+ }
37
+
15
38
  .dropdown-menu {
16
39
  width: max-content;
17
40
  min-width: 100%;
@@ -76,3 +99,16 @@ bs-select {
76
99
  }
77
100
  }
78
101
  }
102
+
103
+ /* stylelint-disable */
104
+ .was-validated bs-select:has(.btn-close) .form-select:invalid,
105
+ .was-validated bs-select:has(.btn-close) .form-select:valid,
106
+ bs-select:has(.btn-close) .form-select.is-invalid,
107
+ bs-select:has(.btn-close) .form-select.is-valid {
108
+ padding-right: 5.5rem !important;
109
+
110
+ + .btn-close {
111
+ left: calc(100% - 4.75rem);
112
+ }
113
+ }
114
+ /* stylelint-enable */
@@ -15,12 +15,14 @@ export default {
15
15
  args: {
16
16
  label: 'Label',
17
17
  options: [
18
- { value: undefined, text: '' },
18
+ { value: undefined, text: 'Select option' },
19
19
  { value: '1', text: 'One', disabled: true },
20
20
  { value: '2', text: 'Two' },
21
21
  { value: '3', text: 'Three', group: 'Group' },
22
22
  ],
23
23
  value: '2',
24
+ floatingLabel: false,
25
+ valid: null,
24
26
  },
25
27
  argTypes: {
26
28
  bsSize: {
@@ -3,6 +3,7 @@ import template from './select.html';
3
3
  import './select.scss';
4
4
 
5
5
  import {
6
+ BsCloseButton,
6
7
  BsDropdown,
7
8
  BsDropdownItem,
8
9
  BsDropdownMenu,
@@ -10,7 +11,7 @@ import {
10
11
  BsSelect as BaseBsSelect,
11
12
  ISelectOption,
12
13
  } from '@ekzo-dev/bootstrap';
13
- import { customElement, ICustomElementViewModel } from 'aurelia';
14
+ import { bindable, customElement, ICustomElementViewModel } from 'aurelia';
14
15
 
15
16
  import { Filter } from './filter';
16
17
 
@@ -22,9 +23,12 @@ const BS_SIZE_MULTIPLIER = {
22
23
  @customElement({
23
24
  name: 'bs-select',
24
25
  template,
25
- dependencies: [BsDropdown, BsDropdownMenu, BsDropdownToggle, BsDropdownItem, Filter],
26
+ dependencies: [BsDropdown, BsDropdownMenu, BsDropdownToggle, BsDropdownItem, Filter, BsCloseButton],
26
27
  })
27
28
  export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
29
+ @bindable()
30
+ emptyValue?: unknown = null;
31
+
28
32
  control!: HTMLFieldSetElement;
29
33
 
30
34
  filter: string = '';
@@ -33,6 +37,8 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
33
37
 
34
38
  deactivating: boolean = false;
35
39
 
40
+ emptyOption?: ISelectOption;
41
+
36
42
  binding() {
37
43
  super.binding();
38
44
  this.deactivating = false;
@@ -64,6 +70,23 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
64
70
 
65
71
  selectOption(option: ISelectOption) {
66
72
  this.value = option.value;
73
+ this.#dispatchEvents();
74
+ }
75
+
76
+ get valueText(): string {
77
+ const { selectedOption, emptyOption } = this;
78
+
79
+ return emptyOption && emptyOption.value === selectedOption?.value
80
+ ? ''
81
+ : `${selectedOption?.group ? selectedOption.group + ' / ' : ''}${selectedOption?.text ?? ''}`;
82
+ }
83
+
84
+ #dispatchEvents() {
85
+ const change = new Event('change', { bubbles: true });
86
+ const input = new Event('input', { bubbles: true });
87
+
88
+ this.control.dispatchEvent(input);
89
+ this.control.dispatchEvent(change);
67
90
  }
68
91
 
69
92
  #setHeight(): void {
@@ -93,8 +116,9 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
93
116
  get selectedOption(): ISelectOption | undefined {
94
117
  if (this['__raw__'].deactivating) return;
95
118
 
96
- const { matcher, value } = this;
119
+ const { matcher, value, emptyValue } = this;
97
120
  let { options } = this;
121
+ let emptyOption: ISelectOption;
98
122
 
99
123
  if (options instanceof Object && options.constructor === Object) {
100
124
  options = Object.entries(options);
@@ -106,12 +130,19 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
106
130
  let option = (options as Array<ISelectOption | readonly [unknown, string]>).find((option) => {
107
131
  const currentValue: unknown = isEntries ? option[0] : (option as ISelectOption).value;
108
132
 
133
+ if (currentValue == emptyValue) {
134
+ emptyOption = {
135
+ value: currentValue,
136
+ text: isEntries ? option[1] : (option as ISelectOption).text,
137
+ } as ISelectOption;
138
+ }
139
+
109
140
  return matcher ? matcher(value, currentValue) : value === currentValue;
110
141
  });
111
142
 
112
143
  option = isEntries && option !== undefined ? { value: option[0], text: option[1] } : (option as ISelectOption);
113
144
 
114
- // update value next tick if it differs from current
145
+ // update value next tick
115
146
  const foundValue = option?.value;
116
147
 
117
148
  if (foundValue !== value) {
@@ -119,6 +150,9 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
119
150
  void Promise.resolve().then(() => (this.value = foundValue));
120
151
  }
121
152
 
153
+ // update empty option next tick
154
+ void Promise.resolve().then(() => (this.emptyOption = emptyOption));
155
+
122
156
  return option;
123
157
  }
124
158
  }