@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 +1 -1
- package/src/forms/duration-input/duration-input.scss +1 -4
- package/src/forms/select/filter.ts +1 -1
- package/src/forms/select/select.html +59 -49
- package/src/forms/select/select.scss +10 -56
- package/src/forms/select/select.stories.ts +15 -7
- package/src/forms/select/select.ts +41 -57
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.
|
|
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",
|
|
@@ -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' : ''}
|
|
1
|
+
<template class="addon ${floatingLabel ? 'form-floating' : ''}" bs-dropdown>
|
|
2
2
|
<label for="${id}" if.bind="label && !floatingLabel" class="form-label">${label}</label>
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
class="form-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
>
|
|
13
|
-
|
|
14
|
-
<
|
|
15
|
-
<
|
|
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"
|
|
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 || ' '}</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 || ' '}</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 || ' '}
|
|
72
82
|
</button>
|
|
73
83
|
</template>
|
|
74
|
-
</
|
|
75
|
-
</
|
|
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/
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
123
|
+
const thisRaw = this['__raw__'] as this;
|
|
124
|
+
|
|
125
|
+
if (thisRaw.deactivating || this.multiple) return;
|
|
145
126
|
|
|
146
|
-
const {
|
|
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
|
|
141
|
+
const optionValue: unknown = isEntries ? option[0] : (option as ISelectOption).value;
|
|
159
142
|
|
|
160
|
-
if (
|
|
143
|
+
if (optionValue == emptyValue) {
|
|
161
144
|
emptyOption = {
|
|
162
|
-
value:
|
|
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,
|
|
150
|
+
return matcher ? matcher(value, optionValue) : value === optionValue;
|
|
168
151
|
});
|
|
169
152
|
|
|
170
|
-
option =
|
|
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;
|