@ekzo-dev/bootstrap-addons 5.2.7 → 5.2.9
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/select/filter.ts +29 -0
- package/src/forms/select/index.ts +1 -0
- package/src/forms/select/select.html +65 -0
- package/src/forms/select/select.scss +71 -0
- package/src/forms/select/select.stories.ts +58 -0
- package/src/forms/select/select.ts +106 -0
- package/src/index.ts +1 -0
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.9",
|
|
5
5
|
"homepage": "https://github.com/ekzo-dev/aurelia-components/tree/main/packages/bootstrap-addons",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ISelectOption } from '@ekzo-dev/bootstrap';
|
|
2
|
+
import { valueConverter } from 'aurelia';
|
|
3
|
+
|
|
4
|
+
@valueConverter('filter')
|
|
5
|
+
export class Filter {
|
|
6
|
+
toView(
|
|
7
|
+
list: Map<string, ISelectOption[]> | ISelectOption[],
|
|
8
|
+
search: string
|
|
9
|
+
): Map<string, ISelectOption[]> | ISelectOption[] {
|
|
10
|
+
const cb = (option: { text: string }) => option.text.toLowerCase().includes(search.toLowerCase());
|
|
11
|
+
|
|
12
|
+
if (list instanceof Map) {
|
|
13
|
+
const result = new Map<string, ISelectOption[]>();
|
|
14
|
+
|
|
15
|
+
list.forEach((v, k) => {
|
|
16
|
+
const matchedOptions = v.filter(cb);
|
|
17
|
+
// const matchedGroup = cb({ text: k });
|
|
18
|
+
|
|
19
|
+
if (matchedOptions.length /* || matchedGroup */) {
|
|
20
|
+
result.set(k, matchedOptions);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return result;
|
|
25
|
+
} else {
|
|
26
|
+
return list.filter(cb);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './select';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<template class="${floatingLabel ? 'form-floating' : ''} ${!multiple ? 'dropdown' : ''}">
|
|
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"
|
|
9
|
+
disabled.bind="disabled"
|
|
10
|
+
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
|
+
class="form-select ${bsSize ? `form-select-${bsSize}` : ''} ${valid ? 'is-valid' : valid === false ? 'is-invalid' : ''}"
|
|
29
|
+
bs-dropdown-toggle="arrow.bind: false"
|
|
30
|
+
value="${selectedOption?.group ? selectedOption.group + ' / ' : ''}${selectedOption?.text}"
|
|
31
|
+
disabled.bind="disabled"
|
|
32
|
+
required.bind="required"
|
|
33
|
+
readonly
|
|
34
|
+
/>
|
|
35
|
+
<bs-dropdown-menu>
|
|
36
|
+
<div bs-dropdown-item="text">
|
|
37
|
+
<input class="form-control" placeholder="Filter options" type="search" value.bind="filter & debounce:200" />
|
|
38
|
+
</div>
|
|
39
|
+
<hr bs-dropdown-item="divider" />
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
repeat.for="option of ungroupedOptions | filter:filter"
|
|
43
|
+
bs-dropdown-item="active.bind: option === selectedOption; disabled.bind: option.disabled"
|
|
44
|
+
click.trigger="selectOption(option)"
|
|
45
|
+
>
|
|
46
|
+
${option.text || ' '}
|
|
47
|
+
</button>
|
|
48
|
+
<template repeat.for="[k, v] of groupedOptions | filter:filter">
|
|
49
|
+
<h6 bs-dropdown-item="header">${k}</h6>
|
|
50
|
+
<button
|
|
51
|
+
class="ps-4"
|
|
52
|
+
type="button"
|
|
53
|
+
repeat.for="option of v"
|
|
54
|
+
bs-dropdown-item="active.bind: option === selectedOption; disabled.bind: option.disabled"
|
|
55
|
+
click.trigger="selectOption(option)"
|
|
56
|
+
>
|
|
57
|
+
${option.text || ' '}
|
|
58
|
+
</button>
|
|
59
|
+
</template>
|
|
60
|
+
</bs-dropdown-menu>
|
|
61
|
+
</template>
|
|
62
|
+
<label for="${id}" if.bind="label && floatingLabel"><span>${label}</span></label>
|
|
63
|
+
<div class="invalid-feedback" if.bind="invalidFeedback">${invalidFeedback}</div>
|
|
64
|
+
<div class="valid-feedback" if.bind="validFeedback">${validFeedback}</div>
|
|
65
|
+
</template>
|
|
@@ -0,0 +1,71 @@
|
|
|
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';
|
|
7
|
+
|
|
8
|
+
bs-select {
|
|
9
|
+
display: block;
|
|
10
|
+
|
|
11
|
+
> .form-select {
|
|
12
|
+
cursor: default;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
> bs-dropdown-menu {
|
|
16
|
+
width: 100%;
|
|
17
|
+
max-height: 100vh;
|
|
18
|
+
overflow-y: auto;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
> .form-control {
|
|
22
|
+
overflow: auto;
|
|
23
|
+
position: relative;
|
|
24
|
+
|
|
25
|
+
.form-check:last-of-type {
|
|
26
|
+
margin-bottom: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
> select {
|
|
30
|
+
position: absolute;
|
|
31
|
+
height: 100%;
|
|
32
|
+
left: 0;
|
|
33
|
+
bottom: 0;
|
|
34
|
+
opacity: 0;
|
|
35
|
+
pointer-events: none;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&.form-floating:not(.dropdown) {
|
|
40
|
+
> label {
|
|
41
|
+
height: auto;
|
|
42
|
+
width: auto;
|
|
43
|
+
transform: none !important;
|
|
44
|
+
left: $input-border-width;
|
|
45
|
+
top: $input-border-width;
|
|
46
|
+
right: 20px;
|
|
47
|
+
font-size: 85%;
|
|
48
|
+
padding-top: 7px;
|
|
49
|
+
padding-bottom: 5px;
|
|
50
|
+
opacity: 1 !important;
|
|
51
|
+
background-color: $input-bg;
|
|
52
|
+
|
|
53
|
+
@include border-radius($input-border-radius, 0);
|
|
54
|
+
|
|
55
|
+
span {
|
|
56
|
+
opacity: 0.65;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
> .form-control {
|
|
61
|
+
min-height: add($form-floating-height, 0.5rem);
|
|
62
|
+
height: auto;
|
|
63
|
+
padding-top: add($form-floating-input-padding-t, 0.5rem) !important;
|
|
64
|
+
padding-bottom: $input-padding-y !important;
|
|
65
|
+
|
|
66
|
+
&:disabled + label {
|
|
67
|
+
background-color: $input-disabled-bg;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { extractArgTypes, Meta, Story } from '@storybook/aurelia';
|
|
2
|
+
|
|
3
|
+
import { selectControl } from '../../../../../.storybook/helpers';
|
|
4
|
+
|
|
5
|
+
import { BsSelect } from '.';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: 'Ekzo / Bootstrap Addons / Forms / Select',
|
|
9
|
+
component: BsSelect,
|
|
10
|
+
parameters: {
|
|
11
|
+
actions: {
|
|
12
|
+
handles: ['change', 'input'],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
label: 'Label',
|
|
17
|
+
options: [
|
|
18
|
+
{ value: undefined, text: '' },
|
|
19
|
+
{ value: '1', text: 'One', disabled: true },
|
|
20
|
+
{ value: '2', text: 'Two' },
|
|
21
|
+
{ value: '3', text: 'Three', group: 'Group' },
|
|
22
|
+
],
|
|
23
|
+
value: '2',
|
|
24
|
+
},
|
|
25
|
+
argTypes: {
|
|
26
|
+
bsSize: {
|
|
27
|
+
...extractArgTypes(BsSelect).bsSize,
|
|
28
|
+
...selectControl(['', 'sm', 'lg']),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
} as Meta;
|
|
32
|
+
|
|
33
|
+
const Overview: Story = (args) => ({
|
|
34
|
+
props: args,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const Multiple: Story = (args) => ({
|
|
38
|
+
props: args,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
Multiple.args = {
|
|
42
|
+
multiple: true,
|
|
43
|
+
value: ['2', '3'],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const LargeOptions: Story = (args) => ({
|
|
47
|
+
props: args,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
LargeOptions.args = {
|
|
51
|
+
options: Array.from({ length: 1000 }).map((v, i) => ({
|
|
52
|
+
value: i,
|
|
53
|
+
text: `Option ${i}`,
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// eslint-disable-next-line
|
|
58
|
+
export { Overview, Multiple, LargeOptions };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import template from './select.html';
|
|
2
|
+
|
|
3
|
+
import './select.scss';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BsDropdown,
|
|
7
|
+
BsDropdownItem,
|
|
8
|
+
BsDropdownMenu,
|
|
9
|
+
BsDropdownToggle,
|
|
10
|
+
BsSelect as BaseBsSelect,
|
|
11
|
+
ISelectOption,
|
|
12
|
+
} from '@ekzo-dev/bootstrap';
|
|
13
|
+
import { coerceBoolean } from '@ekzo-dev/toolkit';
|
|
14
|
+
import { bindable, customElement, ICustomElementViewModel } from 'aurelia';
|
|
15
|
+
|
|
16
|
+
import { Filter } from './filter';
|
|
17
|
+
|
|
18
|
+
const BS_SIZE_MULTIPLIER = {
|
|
19
|
+
lg: 1.125,
|
|
20
|
+
sm: 0.875,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
@customElement({
|
|
24
|
+
name: 'bs-select',
|
|
25
|
+
template,
|
|
26
|
+
dependencies: [BsDropdown, BsDropdownMenu, BsDropdownToggle, BsDropdownItem, Filter],
|
|
27
|
+
})
|
|
28
|
+
export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
|
|
29
|
+
@bindable(coerceBoolean)
|
|
30
|
+
resetUnknownValue: boolean = true;
|
|
31
|
+
|
|
32
|
+
control!: HTMLFieldSetElement;
|
|
33
|
+
|
|
34
|
+
filter: string = '';
|
|
35
|
+
|
|
36
|
+
attached() {
|
|
37
|
+
if (this.multiple) {
|
|
38
|
+
this.#setHeight();
|
|
39
|
+
this.#scrollToSelected();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
propertyChanged(name: keyof this) {
|
|
44
|
+
switch (name) {
|
|
45
|
+
case 'size':
|
|
46
|
+
|
|
47
|
+
case 'bsSize':
|
|
48
|
+
|
|
49
|
+
case 'floatingLabel':
|
|
50
|
+
if (this.multiple) {
|
|
51
|
+
setTimeout(() => this.#setHeight());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
selectOption(option: ISelectOption) {
|
|
57
|
+
this.value = option.value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#setHeight(): void {
|
|
61
|
+
const { style } = this.control;
|
|
62
|
+
|
|
63
|
+
if (this.size > 0) {
|
|
64
|
+
const { borderTopWidth, borderBottomWidth, paddingTop, paddingBottom } = getComputedStyle(this.control);
|
|
65
|
+
|
|
66
|
+
style.height = `calc(${
|
|
67
|
+
this.size * 1.625 * (this.bsSize ? BS_SIZE_MULTIPLIER[this.bsSize] : 1)
|
|
68
|
+
}rem + ${borderTopWidth} + ${borderBottomWidth} + ${paddingTop} + ${paddingBottom} - 2px)`;
|
|
69
|
+
} else if (style.height) {
|
|
70
|
+
style.height = undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#scrollToSelected() {
|
|
75
|
+
const selected = this.control.querySelector<HTMLInputElement>('input:checked');
|
|
76
|
+
|
|
77
|
+
if (selected) {
|
|
78
|
+
const { paddingTop } = getComputedStyle(this.control);
|
|
79
|
+
|
|
80
|
+
this.control.scrollTo({ top: selected.parentElement.offsetTop - parseInt(paddingTop) });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get selectedOption(): ISelectOption | undefined {
|
|
85
|
+
const { matcher, value } = this;
|
|
86
|
+
let { options } = this;
|
|
87
|
+
|
|
88
|
+
if (options instanceof Object && options.constructor === Object) {
|
|
89
|
+
options = Object.entries(options);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const option = (options as Array<ISelectOption | readonly [unknown, string]>).find((option) => {
|
|
93
|
+
const val: unknown = Array.isArray(option) ? option[0] : (option as ISelectOption).value;
|
|
94
|
+
|
|
95
|
+
return matcher ? matcher(value, val) : value === val;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// reset value next tick if needed
|
|
99
|
+
if (option === undefined && value !== undefined && this.resetUnknownValue) {
|
|
100
|
+
console.info('[bootstrap-addons] resetting <bs-select> unknown value');
|
|
101
|
+
void Promise.resolve().then(() => (this.value = undefined));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Array.isArray(option) ? { value: option[0], text: option[1] } : (option as ISelectOption);
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/index.ts
CHANGED