@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 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.6",
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(jsonSchema);
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 ?? '&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';