@ekzo-dev/bootstrap-addons 5.2.27 → 5.2.29

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.27",
4
+ "version": "5.2.29",
5
5
  "homepage": "https://github.com/ekzo-dev/aurelia-components/tree/main/packages/bootstrap-addons",
6
6
  "repository": {
7
7
  "type": "git",
@@ -1,7 +1,4 @@
1
- @import 'bootstrap/scss/functions';
2
- @import 'bootstrap/scss/variables';
3
- @import 'bootstrap/scss/maps';
4
- @import 'bootstrap/scss/mixins';
1
+ @import '@ekzo-dev/bootstrap/src/utils';
5
2
  @import 'bootstrap/scss/forms/form-control';
6
3
 
7
4
  bs-duration-input {
@@ -11,7 +11,7 @@ export class Filter {
11
11
  if (search === '' && emptyOption === undefined) return list;
12
12
 
13
13
  const cb = (option: ISelectOption) =>
14
- option.value !== emptyOption?.value && option.text.toLowerCase().includes(search.toLowerCase());
14
+ option.value !== emptyOption?.value && (option.text ?? '').toLowerCase().includes(search.toLowerCase());
15
15
 
16
16
  if (list instanceof Map) {
17
17
  const result = new Map<string, ISelectOption[]>();
@@ -1,55 +1,65 @@
1
- <template class="${floatingLabel ? 'form-floating' : ''} ${!multiple ? 'dropdown' : ''}">
1
+ <template class="addon ${floatingLabel ? 'form-floating' : ''}" bs-dropdown>
2
2
  <label for="${id}" if.bind="label && !floatingLabel" class="form-label">${label}</label>
3
- <fieldset
4
- if.bind="multiple"
5
- class="form-control ${bsSize ? `form-control-${bsSize}` : ''} ${valid ? 'is-valid' : valid === false ? 'is-invalid' : ''}"
6
- title.bind="title & attr"
7
- name.bind="name & attr"
8
- form.bind="form & attr"
3
+ <input
4
+ id="${id}"
5
+ class="form-select ${bsSize ? `form-select-${bsSize}` : ''} ${valid ? 'is-valid' : valid === false ? 'is-invalid' : ''}"
6
+ bs-dropdown-toggle="arrow.bind: false"
7
+ value="${valueText}"
8
+ placeholder="${emptyOption?.text ?? ''}"
9
9
  disabled.bind="disabled"
10
+ required.bind="required"
11
+ form.bind="form & attr"
12
+ name.bind="name & attr"
13
+ title.bind="title & attr"
14
+ autocomplete="off"
15
+ keydown.trigger="$event.preventDefault()"
10
16
  ref="control"
11
- id="${id}"
12
- >
13
- <select if.bind="required && !value?.length" multiple required></select>
14
- <div class="form-check" repeat.for="option of ungroupedOptions">
15
- <input
16
- class="form-check-input"
17
- type="checkbox"
18
- checked.bind="value"
19
- model.bind="option.value"
20
- disabled.bind="option.disabled"
21
- id="${id+$index}"
22
- />
23
- <label class="form-check-label" for="${id+$index}">${option.text}</label>
24
- </div>
25
- </fieldset>
26
- <template else>
27
- <input
28
- id.bind="id"
29
- class="form-select ${bsSize ? `form-select-${bsSize}` : ''} ${valid ? 'is-valid' : valid === false ? 'is-invalid' : ''}"
30
- bs-dropdown-toggle="arrow.bind: false"
31
- value="${valueText}"
32
- placeholder="${emptyOption?.text ?? ''}"
33
- disabled.bind="disabled"
34
- required.bind="required"
35
- form.bind="form & attr"
36
- name.bind="name & attr"
37
- title.bind="title & attr"
38
- autocomplete="off"
39
- keydown.trigger="$event.preventDefault()"
40
- ref="control"
41
- />
42
- <button
43
- if.bind="emptyOption && selectedOption?.value !== emptyOption?.value && !disabled"
44
- bs-close-button
45
- type="button"
46
- click.trigger="selectOption(emptyOption)"
47
- ></button>
48
- <bs-dropdown-menu popper-config.bind="popperConfig">
49
- <div bs-dropdown-item="text" if.bind="optionsCount > 10">
17
+ />
18
+ <button if.bind="showClear" bs-close-button type="button" click.trigger="clear()"></button>
19
+ <bs-dropdown-menu popper-config.bind="popperConfig" auto-close.bind="multiple ? 'outside' : true">
20
+ <template if.bind="optionsCount > 10">
21
+ <div bs-dropdown-item="text">
50
22
  <input class="form-control" placeholder="Filter options" type="search" value.bind="filter & debounce:250" />
51
23
  </div>
52
- <hr bs-dropdown-item="divider" if.bind="optionsCount > 10" />
24
+ <hr bs-dropdown-item="divider" />
25
+ </template>
26
+ <template if.bind="multiple">
27
+ <div
28
+ repeat.for="option of ungroupedOptions | filter:filter"
29
+ class="dropdown-item ${option.disabled ? 'disabled' : ''}"
30
+ >
31
+ <div class="form-check">
32
+ <input
33
+ class="form-check-input"
34
+ type="checkbox"
35
+ checked.bind="value"
36
+ model.bind="option.value"
37
+ disabled.bind="option.disabled"
38
+ matcher.bind="matcher"
39
+ id="${optionId($index)}"
40
+ />
41
+ <label class="form-check-label" for="${optionId($index)}">${option.text || '&nbsp;'}</label>
42
+ </div>
43
+ </div>
44
+ <template repeat.for="[k, v] of groupedOptions | filter:filter">
45
+ <h6 bs-dropdown-item="header">${k}</h6>
46
+ <div repeat.for="option of v" class="dropdown-item ps-4 ${option.disabled ? 'disabled' : ''}">
47
+ <div class="form-check">
48
+ <input
49
+ class="form-check-input"
50
+ type="checkbox"
51
+ checked.bind="value"
52
+ model.bind="option.value"
53
+ disabled.bind="option.disabled"
54
+ matcher.bind="matcher"
55
+ id="${optionId($index, $parent.$index)}"
56
+ />
57
+ <label class="form-check-label" for="${optionId($index, $parent.$index)}">${option.text || '&nbsp;'}</label>
58
+ </div>
59
+ </div>
60
+ </template>
61
+ </template>
62
+ <template else>
53
63
  <button
54
64
  type="button"
55
65
  repeat.for="option of ungroupedOptions | filter:filter:emptyOption"
@@ -71,8 +81,8 @@
71
81
  ${option.text || '&nbsp;'}
72
82
  </button>
73
83
  </template>
74
- </bs-dropdown-menu>
75
- </template>
84
+ </template>
85
+ </bs-dropdown-menu>
76
86
  <label for="${id}" if.bind="label && floatingLabel"><span>${label}</span></label>
77
87
  <div class="invalid-feedback" if.bind="invalidFeedback">${invalidFeedback}</div>
78
88
  <div class="valid-feedback" if.bind="validFeedback">${validFeedback}</div>
@@ -1,10 +1,7 @@
1
- @import 'bootstrap/scss/functions';
2
- @import 'bootstrap/scss/variables';
3
- @import 'bootstrap/scss/maps';
4
- @import 'bootstrap/scss/mixins';
1
+ @import '@ekzo-dev/bootstrap/src/utils';
5
2
  @import 'bootstrap/scss/forms/form-check';
6
3
 
7
- bs-select {
4
+ bs-select.addon {
8
5
  .form-select {
9
6
  cursor: default;
10
7
  }
@@ -49,63 +46,20 @@ bs-select {
49
46
  white-space: normal;
50
47
  }
51
48
 
52
- > .form-control {
53
- overflow: auto;
54
- position: relative;
55
-
56
- .form-check:last-of-type {
57
- margin-bottom: 0;
58
- }
59
-
60
- > select {
61
- position: absolute;
62
- height: 100%;
63
- left: 0;
64
- bottom: 0;
65
- opacity: 0;
66
- pointer-events: none;
67
- }
68
- }
69
-
70
- &.form-floating:not(.dropdown) {
71
- > label {
72
- height: auto;
73
- width: auto;
74
- transform: none !important;
75
- left: $input-border-width;
76
- top: $input-border-width;
77
- right: 20px;
78
- font-size: 85%;
79
- padding-top: 7px;
80
- padding-bottom: 5px;
81
- opacity: 1 !important;
82
- background-color: $input-bg;
83
-
84
- @include border-radius($input-border-radius, 0);
85
-
86
- span {
87
- opacity: 0.65;
88
- }
89
- }
90
-
91
- > .form-control {
92
- min-height: add($form-floating-height, 0.5rem);
93
- height: auto;
94
- padding-top: add($form-floating-input-padding-t, 0.5rem) !important;
95
- padding-bottom: $input-padding-y !important;
49
+ .form-check {
50
+ margin-bottom: 0;
96
51
 
97
- &:disabled + label {
98
- background-color: $input-disabled-bg;
99
- }
52
+ label {
53
+ display: block;
100
54
  }
101
55
  }
102
56
  }
103
57
 
104
58
  /* stylelint-disable */
105
- .was-validated bs-select:has(.btn-close) .form-select:invalid,
106
- .was-validated bs-select:has(.btn-close) .form-select:valid,
107
- bs-select:has(.btn-close) .form-select.is-invalid,
108
- bs-select:has(.btn-close) .form-select.is-valid {
59
+ .was-validated bs-select.addon:has(.btn-close) .form-select:invalid,
60
+ .was-validated bs-select.addon:has(.btn-close) .form-select:valid,
61
+ bs-select.addon:has(.btn-close) .form-select.is-invalid,
62
+ bs-select.addon:has(.btn-close) .form-select.is-valid {
109
63
  padding-right: 5.5rem !important;
110
64
 
111
65
  + .btn-close {
@@ -15,13 +15,6 @@ export default {
15
15
  },
16
16
  args: {
17
17
  label: 'Label',
18
- options: [
19
- { value: undefined, text: 'Select option' },
20
- { value: '1', text: 'One', disabled: true },
21
- { value: '2', text: 'Two' },
22
- { value: '3', text: 'Three', group: 'Group' },
23
- ],
24
- value: '2',
25
18
  floatingLabel: false,
26
19
  valid: null,
27
20
  },
@@ -37,6 +30,16 @@ const Overview: Story = (args) => ({
37
30
  props: args,
38
31
  });
39
32
 
33
+ Overview.args = {
34
+ options: [
35
+ { value: undefined, text: 'Select option' },
36
+ { value: '1', text: 'One', disabled: true },
37
+ { value: '2', text: 'Two' },
38
+ { value: '3', text: 'Three', group: 'Group' },
39
+ ],
40
+ value: '2',
41
+ };
42
+
40
43
  const Multiple: Story = (args) => ({
41
44
  props: args,
42
45
  });
@@ -44,6 +47,11 @@ const Multiple: Story = (args) => ({
44
47
  Multiple.args = {
45
48
  multiple: true,
46
49
  value: ['2', '3'],
50
+ options: [
51
+ { value: '1', text: 'One', disabled: true },
52
+ { value: '2', text: 'Two' },
53
+ { value: '3', text: 'Three', group: 'Group' },
54
+ ],
47
55
  };
48
56
 
49
57
  const LargeOptions: Story = (args) => ({
@@ -18,11 +18,6 @@ import { bindable, customElement, ICustomElementViewModel, resolve } from 'aurel
18
18
 
19
19
  import { Filter } from './filter';
20
20
 
21
- const BS_SIZE_MULTIPLIER = {
22
- lg: 1.125,
23
- sm: 0.875,
24
- };
25
-
26
21
  @customElement({
27
22
  name: 'bs-select',
28
23
  template,
@@ -49,6 +44,10 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
49
44
  binding() {
50
45
  super.binding();
51
46
  this.deactivating = false;
47
+
48
+ if (this.multiple && !Array.isArray(this.value)) {
49
+ this.value = [];
50
+ }
52
51
  }
53
52
 
54
53
  unbinding() {
@@ -56,30 +55,10 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
56
55
  }
57
56
 
58
57
  attached() {
59
- if (this.multiple) {
60
- this.#setHeight();
61
- this.#scrollToSelected();
62
- }
63
-
64
58
  this.setPopperConfig();
65
59
  }
66
60
 
67
- propertyChanged(name: keyof this) {
68
- switch (name) {
69
- case 'size':
70
-
71
- case 'bsSize':
72
-
73
- case 'floatingLabel':
74
- if (this.multiple) {
75
- setTimeout(() => this.#setHeight());
76
- }
77
- }
78
- }
79
-
80
61
  setPopperConfig() {
81
- if (this.multiple) return;
82
-
83
62
  const { host } = this;
84
63
  const parentModal = host.closest('.modal-body,.popover-body,.offcanvas-body');
85
64
  const dropdownMenu: HTMLElement = host.querySelector('.dropdown-menu');
@@ -95,12 +74,24 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
95
74
  }
96
75
  }
97
76
 
77
+ optionId(index: number, parentIndex?: number): string {
78
+ return `${this.id}${parentIndex != null ? '-g' + parentIndex.toString() : ''}-${index}`;
79
+ }
80
+
98
81
  selectOption(option: ISelectOption) {
99
82
  this.value = option.value;
100
83
  this.#dispatchEvents();
101
84
  }
102
85
 
103
86
  get valueText(): string {
87
+ if (this.multiple) {
88
+ const { options, value } = this;
89
+
90
+ return (value as [])
91
+ .map((val) => (options as ISelectOption[]).find((option) => option.value === val).text)
92
+ .join(', ');
93
+ }
94
+
104
95
  const { selectedOption, emptyOption } = this;
105
96
 
106
97
  return emptyOption && emptyOption.value === selectedOption?.value
@@ -108,6 +99,18 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
108
99
  : `${selectedOption?.group ? selectedOption.group + ' / ' : ''}${selectedOption?.text ?? ''}`;
109
100
  }
110
101
 
102
+ get showClear(): boolean {
103
+ return (
104
+ !this.disabled &&
105
+ ((this.emptyOption && this.selectedOption?.value !== this.emptyOption.value) ||
106
+ (this.multiple && (this.value as unknown[]).length > 0))
107
+ );
108
+ }
109
+
110
+ clear() {
111
+ this.selectOption(this.multiple ? { value: [], text: '' } : this.emptyOption);
112
+ }
113
+
111
114
  #dispatchEvents() {
112
115
  const change = new Event('change', { bubbles: true });
113
116
  const input = new Event('input', { bubbles: true });
@@ -116,34 +119,14 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
116
119
  this.control.dispatchEvent(change);
117
120
  }
118
121
 
119
- #setHeight(): void {
120
- const { style } = this.control;
121
-
122
- if (this.size > 0) {
123
- const { borderTopWidth, borderBottomWidth, paddingTop, paddingBottom } = getComputedStyle(this.control);
124
-
125
- style.height = `calc(${
126
- this.size * 1.625 * (this.bsSize ? BS_SIZE_MULTIPLIER[this.bsSize] : 1)
127
- }rem + ${borderTopWidth} + ${borderBottomWidth} + ${paddingTop} + ${paddingBottom} - 2px)`;
128
- } else if (style.height) {
129
- style.height = undefined;
130
- }
131
- }
132
-
133
- #scrollToSelected() {
134
- const selected = this.control.querySelector<HTMLInputElement>('input:checked');
135
-
136
- if (selected) {
137
- const { paddingTop } = getComputedStyle(this.control);
138
-
139
- this.control.scrollTo({ top: selected.parentElement.offsetTop - parseInt(paddingTop) });
140
- }
141
- }
142
-
143
122
  get selectedOption(): ISelectOption | undefined {
144
- if (this['__raw__'].deactivating) return;
123
+ const thisRaw = this['__raw__'] as this;
124
+
125
+ if (thisRaw.deactivating || this.multiple) return;
145
126
 
146
- const { matcher, value, emptyValue } = this;
127
+ const { value, emptyValue } = this;
128
+ // take matcher from unproxied object to avoid unnecessary getter call
129
+ const { matcher } = thisRaw;
147
130
  let { options } = this;
148
131
  let emptyOption: ISelectOption;
149
132
 
@@ -155,19 +138,20 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
155
138
 
156
139
  const isEntries = Array.isArray(options[0]);
157
140
  let option = (options as Array<ISelectOption | readonly [unknown, string]>).find((option) => {
158
- const currentValue: unknown = isEntries ? option[0] : (option as ISelectOption).value;
141
+ const optionValue: unknown = isEntries ? option[0] : (option as ISelectOption).value;
159
142
 
160
- if (currentValue == emptyValue) {
143
+ if (optionValue == emptyValue) {
161
144
  emptyOption = {
162
- value: currentValue,
163
- text: isEntries ? option[1] : (option as ISelectOption).text,
145
+ value: optionValue,
146
+ text: isEntries ? (option[1] as string) : (option as ISelectOption).text,
164
147
  } as ISelectOption;
165
148
  }
166
149
 
167
- return matcher ? matcher(value, currentValue) : value === currentValue;
150
+ return matcher ? matcher(value, optionValue) : value === optionValue;
168
151
  });
169
152
 
170
- option = isEntries && option !== undefined ? { value: option[0], text: option[1] } : (option as ISelectOption);
153
+ option =
154
+ isEntries && option !== undefined ? { value: option[0], text: option[1] as string } : (option as ISelectOption);
171
155
 
172
156
  // update value next tick
173
157
  const foundValue = option?.value;