@ekzo-dev/bootstrap-addons 5.2.6 → 5.2.8
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/json-input/json-input.ts +3 -3
- 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 +75 -0
- package/src/forms/select/select.stories.ts +57 -0
- package/src/forms/select/select.ts +105 -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.8",
|
|
5
5
|
"homepage": "https://github.com/ekzo-dev/aurelia-components/tree/main/packages/bootstrap-addons",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -141,17 +141,17 @@ export class BsJsonInput {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
get validator(): Validator | undefined {
|
|
144
|
-
const { schemaVersion, disabled, ajvOptions } = this;
|
|
144
|
+
const { schemaVersion, disabled, ajvOptions, jsonSchema } = this;
|
|
145
145
|
// use raw object because proxies don't work with private properties
|
|
146
146
|
const rawThis = this['__raw__'] as BsJsonInput;
|
|
147
147
|
// use jsonSchema from raw object to pass original (non-proxied) object to AJV
|
|
148
|
-
const { jsonSchema } = rawThis;
|
|
148
|
+
const { jsonSchema: rawJsonSchema } = rawThis;
|
|
149
149
|
|
|
150
150
|
if (jsonSchema && typeof jsonSchema === 'object' && !disabled) {
|
|
151
151
|
const ajv = rawThis.#initAjv(jsonSchema.$schema as string, ajvOptions);
|
|
152
152
|
|
|
153
153
|
addFormats(ajv);
|
|
154
|
-
const validate = ajv.compile(
|
|
154
|
+
const validate = ajv.compile(rawJsonSchema);
|
|
155
155
|
|
|
156
156
|
if (validate.errors) {
|
|
157
157
|
throw validate.errors[0];
|
|
@@ -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' : ''}">
|
|
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
|
+
<div else bs-dropdown>
|
|
27
|
+
<button
|
|
28
|
+
class="form-select ${bsSize ? `form-select-${bsSize}` : ''} ${valid ? 'is-valid' : valid === false ? 'is-invalid' : ''}"
|
|
29
|
+
type="button"
|
|
30
|
+
bs-dropdown-toggle="arrow.bind: false"
|
|
31
|
+
disabled.bind="disabled"
|
|
32
|
+
>
|
|
33
|
+
${selectedOption?.group ? selectedOption.group + ' / ' : ''}${selectedOption?.text ?? ' '}
|
|
34
|
+
</button>
|
|
35
|
+
<bs-dropdown-menu>
|
|
36
|
+
<div bs-dropdown-item="text">
|
|
37
|
+
<input class="form-control" placeholder="Filter options" type="search" value.bind="filter & debounce:250" />
|
|
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
|
+
</div>
|
|
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,75 @@
|
|
|
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
|
+
text-align: left;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
bs-dropdown-menu {
|
|
16
|
+
width: 100%;
|
|
17
|
+
max-height: 100vh;
|
|
18
|
+
overflow-y: auto;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.search {
|
|
22
|
+
padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
> .form-control {
|
|
26
|
+
overflow: auto;
|
|
27
|
+
position: relative;
|
|
28
|
+
|
|
29
|
+
.form-check:last-of-type {
|
|
30
|
+
margin-bottom: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
> select {
|
|
34
|
+
position: absolute;
|
|
35
|
+
height: 100%;
|
|
36
|
+
left: 0;
|
|
37
|
+
bottom: 0;
|
|
38
|
+
opacity: 0;
|
|
39
|
+
pointer-events: none;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&.form-floating {
|
|
44
|
+
> label {
|
|
45
|
+
height: auto;
|
|
46
|
+
width: auto;
|
|
47
|
+
transform: none !important;
|
|
48
|
+
left: $input-border-width;
|
|
49
|
+
top: $input-border-width;
|
|
50
|
+
right: 20px;
|
|
51
|
+
font-size: 85%;
|
|
52
|
+
padding-top: 7px;
|
|
53
|
+
padding-bottom: 5px;
|
|
54
|
+
opacity: 1 !important;
|
|
55
|
+
background-color: $input-bg;
|
|
56
|
+
|
|
57
|
+
@include border-radius($input-border-radius, 0);
|
|
58
|
+
|
|
59
|
+
span {
|
|
60
|
+
opacity: 0.65;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
> .form-control {
|
|
65
|
+
min-height: add($form-floating-height, 0.5rem);
|
|
66
|
+
height: auto;
|
|
67
|
+
padding-top: add($form-floating-input-padding-t, 0.5rem) !important;
|
|
68
|
+
padding-bottom: $input-padding-y !important;
|
|
69
|
+
|
|
70
|
+
&:disabled + label {
|
|
71
|
+
background-color: $input-disabled-bg;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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: '1', text: 'One', disabled: true },
|
|
19
|
+
{ value: '2', text: 'Two' },
|
|
20
|
+
{ value: '3', text: 'Three', group: 'Group' },
|
|
21
|
+
],
|
|
22
|
+
value: '2',
|
|
23
|
+
},
|
|
24
|
+
argTypes: {
|
|
25
|
+
bsSize: {
|
|
26
|
+
...extractArgTypes(BsSelect).bsSize,
|
|
27
|
+
...selectControl(['', 'sm', 'lg']),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
} as Meta;
|
|
31
|
+
|
|
32
|
+
const Overview: Story = (args) => ({
|
|
33
|
+
props: args,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const Multiple: Story = (args) => ({
|
|
37
|
+
props: args,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
Multiple.args = {
|
|
41
|
+
multiple: true,
|
|
42
|
+
value: ['2', '3'],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const LargeOptions: Story = (args) => ({
|
|
46
|
+
props: args,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
LargeOptions.args = {
|
|
50
|
+
options: Array.from({ length: 1000 }).map((v, i) => ({
|
|
51
|
+
value: i,
|
|
52
|
+
text: `Option ${i}`,
|
|
53
|
+
})),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// eslint-disable-next-line
|
|
57
|
+
export { Overview, Multiple, LargeOptions };
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
Promise.resolve().then(() => (this.value = undefined));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return Array.isArray(option) ? { value: option[0], text: option[1] } : (option as ISelectOption);
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/index.ts
CHANGED