@ekzo-dev/bootstrap-addons 5.2.24 → 5.2.26

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.24",
4
+ "version": "5.2.26",
5
5
  "homepage": "https://github.com/ekzo-dev/aurelia-components/tree/main/packages/bootstrap-addons",
6
6
  "repository": {
7
7
  "type": "git",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
- "@ekzo-dev/bootstrap": "^5.2.18",
12
+ "@ekzo-dev/bootstrap": "^5.2.20",
13
13
  "@ekzo-dev/vanilla-jsoneditor": "^0.23.7",
14
14
  "@ekzo-dev/toolkit": "^1.2.4",
15
15
  "@fortawesome/free-solid-svg-icons": "^6.5.2",
@@ -21,7 +21,9 @@
21
21
  "immutable-json-patch": "^5.1.3"
22
22
  },
23
23
  "peerDependencies": {
24
- "bootstrap": "^5.2.3"
24
+ "aurelia": "^2.0.0-beta.8",
25
+ "bootstrap": "^5.2.3",
26
+ "@popperjs/core": "^2.11.8"
25
27
  },
26
28
  "main": "src/index.ts",
27
29
  "files": [
@@ -5,11 +5,13 @@ import { valueConverter } from 'aurelia';
5
5
  export class Filter {
6
6
  toView(
7
7
  list: Map<string, ISelectOption[]> | ISelectOption[],
8
- search: string
8
+ search: string,
9
+ emptyOption?: ISelectOption
9
10
  ): Map<string, ISelectOption[]> | ISelectOption[] {
10
- if (search === '') return list;
11
+ if (search === '' && emptyOption === undefined) return list;
11
12
 
12
- const cb = (option: { text: string }) => option.text.toLowerCase().includes(search.toLowerCase());
13
+ const cb = (option: ISelectOption) =>
14
+ option.value !== emptyOption?.value && option.text.toLowerCase().includes(search.toLowerCase());
13
15
 
14
16
  if (list instanceof Map) {
15
17
  const result = new Map<string, ISelectOption[]>();
@@ -28,7 +28,8 @@
28
28
  id.bind="id"
29
29
  class="form-select ${bsSize ? `form-select-${bsSize}` : ''} ${valid ? 'is-valid' : valid === false ? 'is-invalid' : ''}"
30
30
  bs-dropdown-toggle="arrow.bind: false"
31
- value="${selectedOption?.group ? selectedOption.group + ' / ' : ''}${selectedOption?.text ?? ''}"
31
+ value="${valueText}"
32
+ placeholder="${emptyOption?.text ?? ''}"
32
33
  disabled.bind="disabled"
33
34
  required.bind="required"
34
35
  form.bind="form & attr"
@@ -38,26 +39,33 @@
38
39
  keydown.trigger="$event.preventDefault()"
39
40
  ref="control"
40
41
  />
41
- <bs-dropdown-menu>
42
+ <button
43
+ if.bind="emptyOption && selectedOption?.value !== emptyOption?.value"
44
+ bs-close-button
45
+ type="button"
46
+ click.trigger="selectOption(emptyOption)"
47
+ ></button>
48
+ <bs-dropdown-menu popper-config.bind="popperConfig">
42
49
  <div bs-dropdown-item="text" if.bind="optionsCount > 10">
43
50
  <input class="form-control" placeholder="Filter options" type="search" value.bind="filter & debounce:250" />
44
51
  </div>
45
52
  <hr bs-dropdown-item="divider" if.bind="optionsCount > 10" />
46
53
  <button
47
54
  type="button"
48
- repeat.for="option of ungroupedOptions | filter:filter"
49
- bs-dropdown-item="active.bind: option.value === selectedOption.value; disabled.bind: option.disabled"
55
+ repeat.for="option of ungroupedOptions | filter:filter:emptyOption"
56
+ class="dropdown-item ${option.value === selectedOption?.value ? 'active' : ''}"
57
+ disabled.bind="option.disabled"
50
58
  click.trigger="selectOption(option)"
51
59
  >
52
60
  ${option.text || '&nbsp;'}
53
61
  </button>
54
- <template repeat.for="[k, v] of groupedOptions | filter:filter">
62
+ <template repeat.for="[k, v] of groupedOptions | filter:filter:emptyOption">
55
63
  <h6 bs-dropdown-item="header">${k}</h6>
56
64
  <button
57
- class="ps-4"
58
65
  type="button"
59
66
  repeat.for="option of v"
60
- bs-dropdown-item="active.bind: option.value === selectedOption.value; disabled.bind: option.disabled"
67
+ class="ps-4 dropdown-item ${option.value === selectedOption?.value ? 'active' : ''}"
68
+ disabled.bind="option.disabled"
61
69
  click.trigger="selectOption(option)"
62
70
  >
63
71
  ${option.text || '&nbsp;'}
@@ -1,17 +1,40 @@
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';
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-check';
7
6
 
8
7
  bs-select {
9
- display: block;
10
-
11
- > .form-select {
8
+ .form-select {
12
9
  cursor: default;
13
10
  }
14
11
 
12
+ &:has(.btn-close) .form-select {
13
+ padding-right: 4rem;
14
+ }
15
+
16
+ .btn-close {
17
+ display: block;
18
+ position: relative;
19
+ box-sizing: border-box;
20
+ width: 0.8rem;
21
+ height: 0.8rem;
22
+ top: calc(-1px - 1.5rem);
23
+ left: calc(100% - 3.25rem);
24
+ }
25
+
26
+ .form-select-lg + .btn-close {
27
+ top: calc(-1px - 1.8rem);
28
+ }
29
+
30
+ .form-select-sm + .btn-close {
31
+ top: calc(-1px - 1.3rem);
32
+ }
33
+
34
+ &.form-floating .btn-close {
35
+ top: calc(-1px - 2.15rem) !important;
36
+ }
37
+
15
38
  .dropdown-menu {
16
39
  width: max-content;
17
40
  min-width: 100%;
@@ -76,3 +99,16 @@ bs-select {
76
99
  }
77
100
  }
78
101
  }
102
+
103
+ /* stylelint-disable */
104
+ .was-validated bs-select:has(.btn-close) .form-select:invalid,
105
+ .was-validated bs-select:has(.btn-close) .form-select:valid,
106
+ bs-select:has(.btn-close) .form-select.is-invalid,
107
+ bs-select:has(.btn-close) .form-select.is-valid {
108
+ padding-right: 5.5rem !important;
109
+
110
+ + .btn-close {
111
+ left: calc(100% - 4.75rem);
112
+ }
113
+ }
114
+ /* stylelint-enable */
@@ -1,3 +1,4 @@
1
+ import { BsButton, BsOffcanvas } from '@ekzo-dev/bootstrap';
1
2
  import { extractArgTypes, Meta, Story } from '@storybook/aurelia';
2
3
 
3
4
  import { selectControl } from '../../../../../.storybook/helpers';
@@ -15,12 +16,14 @@ export default {
15
16
  args: {
16
17
  label: 'Label',
17
18
  options: [
18
- { value: undefined, text: '' },
19
+ { value: undefined, text: 'Select option' },
19
20
  { value: '1', text: 'One', disabled: true },
20
21
  { value: '2', text: 'Two' },
21
22
  { value: '3', text: 'Three', group: 'Group' },
22
23
  ],
23
24
  value: '2',
25
+ floatingLabel: false,
26
+ valid: null,
24
27
  },
25
28
  argTypes: {
26
29
  bsSize: {
@@ -55,5 +58,23 @@ LargeOptions.args = {
55
58
  })),
56
59
  };
57
60
 
61
+ const InModal: Story = (args) => ({
62
+ props: args,
63
+ template: `
64
+ <button bs-button click.trigger="offcanvas.toggle()">Open modal</button>
65
+ <bs-offcanvas component.ref="offcanvas">
66
+ <bs-select value.bind="value" options.bind="options" label.bind="label" style="width: 100%"></bs-select>
67
+ <div style="height: 2000px"></div>
68
+ </bs-offcanvas>`,
69
+ components: [BsOffcanvas, BsButton],
70
+ });
71
+
72
+ InModal.args = {
73
+ options: Array.from({ length: 1000 }).map((v, i) => ({
74
+ value: i.toString(),
75
+ text: `Option ${i} has long content which forces dropdown menu to scale larger that select box`,
76
+ })),
77
+ };
78
+
58
79
  // eslint-disable-next-line
59
- export { Overview, Multiple, LargeOptions };
80
+ export { Overview, Multiple, LargeOptions, InModal };
@@ -2,7 +2,11 @@ import template from './select.html';
2
2
 
3
3
  import './select.scss';
4
4
 
5
+ import type { Options } from '@popperjs/core';
6
+ import type { Tooltip } from 'bootstrap';
7
+
5
8
  import {
9
+ BsCloseButton,
6
10
  BsDropdown,
7
11
  BsDropdownItem,
8
12
  BsDropdownMenu,
@@ -10,7 +14,7 @@ import {
10
14
  BsSelect as BaseBsSelect,
11
15
  ISelectOption,
12
16
  } from '@ekzo-dev/bootstrap';
13
- import { customElement, ICustomElementViewModel } from 'aurelia';
17
+ import { bindable, customElement, ICustomElementViewModel, resolve } from 'aurelia';
14
18
 
15
19
  import { Filter } from './filter';
16
20
 
@@ -22,9 +26,14 @@ const BS_SIZE_MULTIPLIER = {
22
26
  @customElement({
23
27
  name: 'bs-select',
24
28
  template,
25
- dependencies: [BsDropdown, BsDropdownMenu, BsDropdownToggle, BsDropdownItem, Filter],
29
+ dependencies: [BsDropdown, BsDropdownMenu, BsDropdownToggle, BsDropdownItem, Filter, BsCloseButton],
26
30
  })
27
31
  export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
32
+ @bindable()
33
+ emptyValue?: unknown = null;
34
+
35
+ host = resolve(HTMLElement);
36
+
28
37
  control!: HTMLFieldSetElement;
29
38
 
30
39
  filter: string = '';
@@ -33,6 +42,10 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
33
42
 
34
43
  deactivating: boolean = false;
35
44
 
45
+ emptyOption?: ISelectOption;
46
+
47
+ popperConfig: Partial<Options> | Tooltip.PopperConfigFunction | null = null;
48
+
36
49
  binding() {
37
50
  super.binding();
38
51
  this.deactivating = false;
@@ -47,6 +60,8 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
47
60
  this.#setHeight();
48
61
  this.#scrollToSelected();
49
62
  }
63
+
64
+ this.setPopperConfig();
50
65
  }
51
66
 
52
67
  propertyChanged(name: keyof this) {
@@ -62,11 +77,37 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
62
77
  }
63
78
  }
64
79
 
80
+ setPopperConfig() {
81
+ if (this.multiple) return;
82
+
83
+ const { host } = this;
84
+ const parentModal = host.closest('.modal-body,.popover-body,.offcanvas-body');
85
+ const dropdownMenu: HTMLElement = host.querySelector('.dropdown-menu');
86
+
87
+ if (parentModal != null) {
88
+ this.popperConfig = {
89
+ strategy: 'fixed',
90
+ };
91
+ dropdownMenu.style.minWidth = `${host.offsetWidth}px`;
92
+ } else {
93
+ this.popperConfig = null;
94
+ dropdownMenu.style.minWidth = undefined;
95
+ }
96
+ }
97
+
65
98
  selectOption(option: ISelectOption) {
66
99
  this.value = option.value;
67
100
  this.#dispatchEvents();
68
101
  }
69
102
 
103
+ get valueText(): string {
104
+ const { selectedOption, emptyOption } = this;
105
+
106
+ return emptyOption && emptyOption.value === selectedOption?.value
107
+ ? ''
108
+ : `${selectedOption?.group ? selectedOption.group + ' / ' : ''}${selectedOption?.text ?? ''}`;
109
+ }
110
+
70
111
  #dispatchEvents() {
71
112
  const change = new Event('change', { bubbles: true });
72
113
  const input = new Event('input', { bubbles: true });
@@ -102,8 +143,9 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
102
143
  get selectedOption(): ISelectOption | undefined {
103
144
  if (this['__raw__'].deactivating) return;
104
145
 
105
- const { matcher, value } = this;
146
+ const { matcher, value, emptyValue } = this;
106
147
  let { options } = this;
148
+ let emptyOption: ISelectOption;
107
149
 
108
150
  if (options instanceof Object && options.constructor === Object) {
109
151
  options = Object.entries(options);
@@ -115,12 +157,19 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
115
157
  let option = (options as Array<ISelectOption | readonly [unknown, string]>).find((option) => {
116
158
  const currentValue: unknown = isEntries ? option[0] : (option as ISelectOption).value;
117
159
 
160
+ if (currentValue == emptyValue) {
161
+ emptyOption = {
162
+ value: currentValue,
163
+ text: isEntries ? option[1] : (option as ISelectOption).text,
164
+ } as ISelectOption;
165
+ }
166
+
118
167
  return matcher ? matcher(value, currentValue) : value === currentValue;
119
168
  });
120
169
 
121
170
  option = isEntries && option !== undefined ? { value: option[0], text: option[1] } : (option as ISelectOption);
122
171
 
123
- // update value next tick if it differs from current
172
+ // update value next tick
124
173
  const foundValue = option?.value;
125
174
 
126
175
  if (foundValue !== value) {
@@ -128,6 +177,9 @@ export class BsSelect extends BaseBsSelect implements ICustomElementViewModel {
128
177
  void Promise.resolve().then(() => (this.value = foundValue));
129
178
  }
130
179
 
180
+ // update empty option next tick
181
+ void Promise.resolve().then(() => (this.emptyOption = emptyOption));
182
+
131
183
  return option;
132
184
  }
133
185
  }