@ekzo-dev/bootstrap-addons 5.2.7 → 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 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.7",
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",
@@ -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 ?? '&nbsp;'}
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
@@ -1 +1,2 @@
1
1
  export * from './forms/json-input';
2
+ export * from './forms/select';