@kws3/ui 1.8.1 → 1.8.4

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/CHANGELOG.mdx CHANGED
@@ -1,3 +1,20 @@
1
+ ## 1.8.4
2
+ - New `AutoComplete` component
3
+ - Make options text size match the input `size` in `MultiSelect` and `SearchableSelect`.
4
+ - Prevent default arrow up/down behaviour on `MultiSelect` and `SearchableSelect` when options dropdown is open.
5
+
6
+ ## 1.8.3
7
+ - Allow `clickableRows` and `bulk_actions` to work at the same time on `GridView`
8
+ - Various bugfixes on `GridRow`
9
+ - New `visualActivationOnClick` prop for `GridView` and `TileView`
10
+ - Change the way click activation works on `GridView` and `TileView` rows. Now only one row can be activated at a time
11
+
12
+ ## 1.8.2
13
+ - Usability fixes for `NumberInput`
14
+ - New `input_only`, `force_integer`, `style` and `class` props for `NumberInput`
15
+ - Forward `focus`, `blur` input events for `NumberInput`
16
+ - Use custom version of `fuzzysearch` for `SearchableSelect` and `MultiSelect`
17
+
1
18
  ## 1.8.1
2
19
  - New `ScrollableList` component
3
20
 
@@ -20,52 +20,84 @@ This will be overridden if `min` is higher, or `max` is lower, Default: `0`
20
20
  @param {string} [plus_icon="plus"] - Name of the icon that is to be displayed in the plus button, Default: `"plus"`
21
21
  @param {''|'success'|'primary'|'warning'|'info'|'danger'|'dark'|'light'} [plus_icon_color="success"] - Color of the Plus Icon, Default: `"success"`
22
22
  @param {''|'success'|'primary'|'warning'|'info'|'danger'|'dark'|'light'} [plus_button_color=""] - Color of the Plus Button, Default: `""`
23
+ @param {boolean} [input_only=false] - Show input without controls, Default: `false`
24
+ @param {boolean} [force_integer=false] - Prevent decimal numbers such as `1.5`, Default: `false`
25
+ @param {string} [style=""] - Inline CSS for component, Default: `""`
26
+ @param {string} [class=""] - CSS classes for component, Default: `""`
23
27
 
24
28
  ### Events
25
29
  - `change` - Triggered when value changes
30
+ - `blur`
31
+ - `focus`
26
32
 
27
33
  -->
28
- <div class="field has-addons">
29
- <div class="control">
30
- <button
31
- type="button"
32
- class="button is-{size} is-{minus_button_color}"
33
- style="box-shadow:none;"
34
- on:click={count(-1)}
35
- disabled={disabled || value <= min}>
36
- <Icon
37
- icon={minus_icon}
38
- size="small"
39
- class="has-text-{minus_icon_color}" />
40
- </button>
34
+ {#if input_only}
35
+ <input
36
+ {style}
37
+ data-testid="input"
38
+ class="input has-text-centered {klass} is-{size} is-{value < min ||
39
+ value > max
40
+ ? 'danger'
41
+ : ''}"
42
+ type="number"
43
+ min
44
+ max
45
+ {step}
46
+ {disabled}
47
+ readonly={!typeable}
48
+ bind:value
49
+ on:blur={isBlurred}
50
+ on:blur
51
+ on:focus={isFocused}
52
+ on:focus />
53
+ {:else}
54
+ <div class="field has-addons {klass}" {style}>
55
+ <div class="control">
56
+ <button
57
+ type="button"
58
+ class="button is-{size} is-{minus_button_color}"
59
+ style="box-shadow:none;"
60
+ on:click={count(-1)}
61
+ disabled={disabled || value <= min}>
62
+ <Icon
63
+ icon={minus_icon}
64
+ size="small"
65
+ class="has-text-{minus_icon_color}" />
66
+ </button>
67
+ </div>
68
+ <div class="control is-{fullwidth ? 'expanded' : 'narrow'}">
69
+ <input
70
+ data-testid="input"
71
+ class="input has-text-centered is-{size} is-{value < min || value > max
72
+ ? 'danger'
73
+ : ''}"
74
+ type="number"
75
+ min
76
+ max
77
+ {step}
78
+ {disabled}
79
+ readonly={!typeable}
80
+ bind:value
81
+ on:blur={isBlurred}
82
+ on:blur
83
+ on:focus={isFocused}
84
+ on:focus />
85
+ </div>
86
+ <div class="control">
87
+ <button
88
+ type="button"
89
+ class="button is-{size} is-{plus_button_color}"
90
+ style="box-shadow:none;"
91
+ on:click|preventDefault={count(+1)}
92
+ disabled={disabled || value >= max}>
93
+ <Icon
94
+ icon={plus_icon}
95
+ size="small"
96
+ class="has-text-{plus_icon_color}" />
97
+ </button>
98
+ </div>
41
99
  </div>
42
- <div class="control is-{fullwidth ? 'expanded' : 'narrow'}">
43
- <input
44
- data-testid="input"
45
- class="input has-text-centered is-{size} is-{value < min || value > max
46
- ? 'danger'
47
- : ''}"
48
- type="number"
49
- min
50
- max
51
- step
52
- {disabled}
53
- readonly={!typeable}
54
- bind:value
55
- on:blur={isBlurred()}
56
- on:focus={isFocused()} />
57
- </div>
58
- <div class="control">
59
- <button
60
- type="button"
61
- class="button is-{size} is-{plus_button_color}"
62
- style="box-shadow:none;"
63
- on:click|preventDefault={count(+1)}
64
- disabled={disabled || value >= max}>
65
- <Icon icon={plus_icon} size="small" class="has-text-{plus_icon_color}" />
66
- </button>
67
- </div>
68
- </div>
100
+ {/if}
69
101
 
70
102
  <style>
71
103
  input[type="number"]::-webkit-inner-spin-button,
@@ -158,7 +190,25 @@ This will be overridden if `min` is higher, or `max` is lower, Default: `0`
158
190
  * Color of the Plus Button
159
191
  * @type {''|'success'|'primary'|'warning'|'info'|'danger'|'dark'|'light'}
160
192
  */
161
- plus_button_color = "";
193
+ plus_button_color = "",
194
+ /**
195
+ * Show input without controls
196
+ */
197
+ input_only = false,
198
+ /**
199
+ * Prevent decimal numbers such as `1.5`
200
+ */
201
+ force_integer = false,
202
+ /**
203
+ * Inline CSS for component
204
+ */
205
+ style = "";
206
+
207
+ /**
208
+ * CSS classes for component
209
+ */
210
+ let klass = "";
211
+ export { klass as class };
162
212
 
163
213
  let _has_focus = false,
164
214
  _old_value = null;
@@ -189,6 +239,8 @@ This will be overridden if `min` is higher, or `max` is lower, Default: `0`
189
239
 
190
240
  if (typeof value == "undefined" || value === null) value = min;
191
241
 
242
+ if (force_integer) value = Math.floor(Number(value));
243
+
192
244
  if (value < min) value = min;
193
245
  if (value > max) value = max;
194
246
 
@@ -4,7 +4,8 @@
4
4
 
5
5
  @param {number} [row_index=0] - Row index value, Default: `0`
6
6
  @param {object} [row={}] - Contains all the column values in a row, Default: `{}`
7
- @param {boolean} [rowActive=false] - Determines whether the row is selected or not, Default: `false`
7
+ @param {boolean} [visualActivationOnClick=true] - Determines whether clickable rows activate visually on click, Default: `true`
8
+ @param {object} [activatedId=null] - Unique id of row that is activated, Default: `null`
8
9
  @param {object} [isVisible={}] - Determines whether column is visible or not, Default: `{}`
9
10
  @param {boolean} [clickableRows=false] - Determines whether the row is clickable or not, Default: `false`
10
11
  @param {object} [transforms={}] - Contains all custom values for each columns, Default: `{}`
@@ -29,10 +30,14 @@
29
30
  <tr
30
31
  in:fly={{ x: 20, delay: 25 * row_index }}
31
32
  on:click|stopPropagation={rowClick}
32
- class:is-selected={rowActive}
33
+ class:is-selected={activated && visualActivationOnClick}
33
34
  class:is-checked={checked}>
34
35
  {#if bulk_actions}
35
- <td style="vertical-align:middle;">
36
+ <td
37
+ style="vertical-align:middle;"
38
+ on:click={(e) => {
39
+ clickableRows && e.stopImmediatePropagation();
40
+ }}>
36
41
  <Checkbox
37
42
  size={selectCheckboxSize}
38
43
  color={selectCheckboxColor}
@@ -58,7 +63,7 @@
58
63
  {:else}
59
64
  <tr
60
65
  on:click|stopPropagation={rowClick}
61
- class:is-selected={rowActive}
66
+ class:is-selected={activated && visualActivationOnClick}
62
67
  class:is-checked={checked}>
63
68
  {#if bulk_actions}
64
69
  <td style="vertical-align:middle;">
@@ -103,9 +108,13 @@
103
108
  */
104
109
  row = {},
105
110
  /**
106
- * Determines whether the row is selected or not
111
+ * Determines whether clickable rows activate visually on click
112
+ */
113
+ visualActivationOnClick = true,
114
+ /**
115
+ * Unique id of row that is activated
107
116
  */
108
- rowActive = false,
117
+ activatedId = null,
109
118
  /**
110
119
  * Determines whether column is visible or not
111
120
  */
@@ -161,6 +170,7 @@
161
170
  };
162
171
 
163
172
  $: selectedIds, setCheckedValue();
173
+ $: activated = activatedId === row.id;
164
174
 
165
175
  function setCheckedValue() {
166
176
  checked = false;
@@ -175,7 +185,6 @@
175
185
 
176
186
  function rowClick() {
177
187
  if (clickableRows) {
178
- rowActive = true;
179
188
  fire("rowClick", { row });
180
189
  }
181
190
  }
@@ -5,9 +5,11 @@
5
5
  @param {string} [iteration_key="id"] - Iteration key, Default: `"id"`
6
6
  @param {array} [data=[]] - Contains all the results that needs to be displayed, Default: `[]`
7
7
  @param {object} [columns={}] - Table column names. {db_field_name: column_name}, Default: `{}`
8
- @param {boolean} [transition=false] - Determines if a transision effect is used, Default: `false`
8
+ @param {boolean} [transition=false] - Determines if a transition effect is used, Default: `false`
9
9
  @param {boolean} [is_striped=true] - Determines whether to use alternating row shading in the table view, Default: `true`
10
+ @param {boolean} [visualActivationOnClick=true] - Determines whether clickable rows activate visually on click, Default: `true`
10
11
  @param {boolean} [clickableRows=false] - Determines whether rows are clickable or not, Default: `false`
12
+ @param {object} [activatedId=null] - Unique id of row that is activated, Default: `null`
11
13
  @param {boolean} [bulk_actions=false] - Determines if selecting multiple rows and doing multiple actions is allowed, Default: `false`
12
14
  @param {boolean} [selectAll=false] - Determines if all rows are selected, Default: `false`
13
15
  @param {array} [selectedIds=[]] - List of unique IDs of all the selected rows, Default: `[]`
@@ -64,6 +66,7 @@
64
66
  {transition}
65
67
  {column_keys}
66
68
  {clickableRows}
69
+ {visualActivationOnClick}
67
70
  {isVisible}
68
71
  {transforms}
69
72
  {classNames}
@@ -71,6 +74,7 @@
71
74
  {cellComponent}
72
75
  {row}
73
76
  {bulk_actions}
77
+ {activatedId}
74
78
  {selectedIds}
75
79
  {selectCheckboxColor}
76
80
  {selectCheckboxSize}
@@ -100,7 +104,7 @@
100
104
  */
101
105
  columns = {},
102
106
  /**
103
- * Determines if a transision effect is used
107
+ * Determines if a transition effect is used
104
108
  */
105
109
  transition = false,
106
110
  /**
@@ -108,10 +112,18 @@
108
112
  * @link https://bulma.io/documentation/elements/table/#modifiers
109
113
  */
110
114
  is_striped = true,
115
+ /**
116
+ * Determines whether clickable rows activate visually on click
117
+ */
118
+ visualActivationOnClick = true,
111
119
  /**
112
120
  * Determines whether rows are clickable or not
113
121
  */
114
122
  clickableRows = false,
123
+ /**
124
+ * Unique id of row that is activated
125
+ */
126
+ activatedId = null,
115
127
  /**
116
128
  * Determines if selecting multiple rows and doing multiple actions is allowed
117
129
  */
@@ -8,7 +8,9 @@
8
8
  @param {object} [tileItemComponent=null] - Contains a custom component, Default: `null`
9
9
  @param {number} [per_row=3] - Sets how many items to display in a row, Default: `3`
10
10
  @param {object} [columns={}] - Column names for the displayed table {db_field_name: column_name}, Default: `{}`
11
+ @param {boolean} [visualActivationOnClick=true] - Determines whether clickable rows activate visually on click, Default: `true`
11
12
  @param {boolean} [clickableRows=false] - Determines whether rows are clickable or not, Default: `false`
13
+ @param {object} [activatedId=null] - Unique id of row that is activated, Default: `null`
12
14
  @param {object} [valueTransformers={}] - Contains all custom values for each column, Default: `{}`
13
15
  @param {object} [classTransformers={}] - CSS class names for each column, Default: `{}`
14
16
  @param {object} [styleTransformers={}] - CSS styles for each column, Default: `{}`
@@ -31,6 +33,7 @@
31
33
  on:_forwardEvent
32
34
  {row_index}
33
35
  {column_keys}
36
+ {visualActivationOnClick}
34
37
  {clickableRows}
35
38
  {isVisible}
36
39
  {transforms}
@@ -38,6 +41,7 @@
38
41
  {styles}
39
42
  {row}
40
43
  {bulk_actions}
44
+ {activatedId}
41
45
  {selectedIds}
42
46
  {selectCheckboxColor}
43
47
  {selectCheckboxSize}
@@ -75,10 +79,18 @@
75
79
  * Column names for the displayed table {db_field_name: column_name}
76
80
  */
77
81
  columns = {},
82
+ /**
83
+ * Determines whether clickable rows activate visually on click
84
+ */
85
+ visualActivationOnClick = true,
78
86
  /**
79
87
  * Determines whether rows are clickable or not
80
88
  */
81
89
  clickableRows = false,
90
+ /**
91
+ * Unique id of row that is activated
92
+ */
93
+ activatedId = null,
82
94
  /**
83
95
  * Contains all custom values for each column
84
96
  */
@@ -3,7 +3,8 @@
3
3
 
4
4
 
5
5
  @param {object} [row={}] - List of all values in a row, Default: `{}`
6
- @param {boolean} [rowActive=false] - Determines whether the row is selected or not, Default: `false`
6
+ @param {boolean} [visualActivationOnClick=true] - Determines whether clickable rows activate visually on click, Default: `true`
7
+ @param {object} [activatedId=null] - Unique id of row that is activated, Default: `null`
7
8
  @param {boolean} [clickableRows=false] - Determines whether the row is clickable or not, Default: `false`
8
9
  @param {function} [isVisible()] - Returns whether a column can be visible or not
9
10
  @param {function} [transforms()] - Returns column custom value
@@ -16,7 +17,7 @@
16
17
 
17
18
  -->
18
19
  <div
19
- class:is-selected={rowActive}
20
+ class:is-selected={activated && visualActivationOnClick}
20
21
  class="box {clickableRows ? 'is-hoverable' : ''}"
21
22
  on:click|stopPropagation={rowClick}>
22
23
  {#each column_keys as column}
@@ -39,9 +40,13 @@
39
40
  */
40
41
  export let row = {},
41
42
  /**
42
- * Determines whether the row is selected or not
43
+ * Determines whether clickable rows activate visually on click
43
44
  */
44
- rowActive = false,
45
+ visualActivationOnClick = true,
46
+ /**
47
+ * Unique id of row that is activated
48
+ */
49
+ activatedId = null,
45
50
  /**
46
51
  * Determines whether the row is clickable or not
47
52
  */
@@ -67,9 +72,10 @@
67
72
  */
68
73
  column_keys = [];
69
74
 
75
+ $: activated = activatedId === row.id;
76
+
70
77
  function rowClick() {
71
78
  if (clickableRows) {
72
- rowActive = true;
73
79
  /**
74
80
  * Fires an event when a row is clicked
75
81
  */
@@ -0,0 +1,426 @@
1
+ <!--
2
+ @component
3
+
4
+
5
+ @param {string} [value=""] - Value of the Input
6
+
7
+ This property can be bound to, to fetch the current value, Default: `""`
8
+ @param {string} [placeholder=""] - Placeholder text for the input, Default: `""`
9
+ @param {array} [options=[]] - Array of strings, or objects.
10
+ Used to populate the list of options in the dropdown, Default: `[]`
11
+ @param {function|null} [search=null] - Async function to fetch options
12
+
13
+ Only send this prop if you want to fetch `options` asynchronously.
14
+ `options` prop will be ignored if this prop is set., Default: `null`
15
+ @param {'fuzzy'|'strict'} [search_strategy="fuzzy"] - Filtered options to be displayed strictly based on search text or perform a fuzzy match.
16
+ Fuzzy match will not work if `search` function is set, as the backend service is meant to do the matching., Default: `"fuzzy"`
17
+ @param {boolean} [highlighted_results=true] - Whether to show the highlighted or plain results in the dropdown., Default: `true`
18
+ @param {''|'small'|'medium'|'large'} [size=""] - Size of the input, Default: `""`
19
+ @param {''|'primary'|'success'|'warning'|'info'|'danger'|'dark'|'light'} [color=""] - Color of the input, Default: `""`
20
+ @param {string} [style=""] - Inline CSS for input container, Default: `""`
21
+ @param {boolean} [readonly=false] - Marks component as read-only, Default: `false`
22
+ @param {boolean} [disabled=false] - Disables the component, Default: `false`
23
+ @param {HTMLElement|string} [dropdown_portal=undefined] - Where to render the dropdown list.
24
+ Can be a DOM element or a `string` with the CSS selector of the element.
25
+
26
+ By default it renders in a custom container appended to `document.body`., Default: `undefined`
27
+ @param {string} [class=""] - CSS classes for input container, Default: `""`
28
+
29
+ ### Events
30
+ - `change`
31
+ - `blur` - Triggered when the input loses focus
32
+
33
+ ### Slots
34
+ - `<slot name="default" {option} />` - Slot containing text for each selectable item
35
+
36
+ Default value: `<span>{option.label}</span>`
37
+
38
+ -->
39
+ <div
40
+ bind:this={el}
41
+ class="
42
+ kws-autocomplete input
43
+ {disabled ? 'is-disabled' : ''}
44
+ {readonly ? 'is-readonly' : ''}
45
+ is-{size} is-{color} {klass}
46
+ "
47
+ class:readonly
48
+ {style}
49
+ on:click|stopPropagation={() => input && input.focus()}>
50
+ <input
51
+ class="input is-{size}"
52
+ bind:this={input}
53
+ autocomplete="off"
54
+ {disabled}
55
+ {readonly}
56
+ bind:value
57
+ on:keydown={handleKeydown}
58
+ on:blur={blurEvent}
59
+ on:blur={() => setOptionsVisible(false)}
60
+ {placeholder} />
61
+ {#if search && options_loading}
62
+ <button
63
+ type="button"
64
+ style="border: none;"
65
+ class="button is-paddingless delete is-medium is-loading" />
66
+ {/if}
67
+ {#if rootContainer}
68
+ <div class="kws-autocomplete" use:portal={dropdown_portal}>
69
+ <ul bind:this={dropdown} class="options" class:hidden={!show_options}>
70
+ {#each filtered_options as option}
71
+ <li
72
+ on:mousedown|preventDefault|stopPropagation={() =>
73
+ handleOptionMouseDown(option)}
74
+ on:mouseenter|preventDefault|stopPropagation={() => {
75
+ active_option = option;
76
+ }}
77
+ class="is-size-{list_text_size[size]}"
78
+ class:active={active_option === option}>
79
+ <!--
80
+ Slot containing text for each selectable item
81
+
82
+ Default value: `<span>{option.label}</span>`
83
+ -->
84
+ <slot {option}>
85
+ <!-- eslint-disable-next-line @ota-meshi/svelte/no-at-html-tags -->
86
+ {@html option.label}
87
+ </slot>
88
+ </li>
89
+ {/each}
90
+ </ul>
91
+ </div>
92
+ {/if}
93
+ </div>
94
+
95
+ <script>
96
+ import { portal } from "@kws3/ui";
97
+ import { debounce } from "@kws3/ui/utils";
98
+ import { createEventDispatcher, onMount, tick } from "svelte";
99
+ import { createPopper } from "@popperjs/core";
100
+ import fuzzysearch from "@kws3/ui/utils/fuzzysearch";
101
+
102
+ const sameWidthPopperModifier = {
103
+ name: "sameWidth",
104
+ enabled: true,
105
+ phase: "beforeWrite",
106
+ requires: ["computeStyles"],
107
+ fn: ({ state }) => {
108
+ state.styles.popper.width = `${Math.max(
109
+ 200,
110
+ state.rects.reference.width
111
+ )}px`;
112
+ },
113
+ effect: ({ state }) => {
114
+ state.elements.popper.style.width = `${state.elements.reference.offsetWidth}px`;
115
+ },
116
+ };
117
+
118
+ const rootContainerId = "kws-overlay-root";
119
+
120
+ /**
121
+ * Value of the Input
122
+ *
123
+ * This property can be bound to, to fetch the current value
124
+ */
125
+ export let value = "";
126
+
127
+ /**
128
+ * Placeholder text for the input
129
+ */
130
+ export let placeholder = "";
131
+ /**
132
+ * Array of strings, or objects.
133
+ * Used to populate the list of options in the dropdown
134
+ */
135
+ export let options = [];
136
+ /**
137
+ * Async function to fetch options
138
+ *
139
+ * Only send this prop if you want to fetch `options` asynchronously.
140
+ * `options` prop will be ignored if this prop is set.
141
+ *
142
+ * @type {function|null}
143
+ */
144
+ export let search = null;
145
+
146
+ /**
147
+ * Filtered options to be displayed strictly based on search text or perform a fuzzy match.
148
+ * Fuzzy match will not work if `search` function is set, as the backend service is meant to do the matching.
149
+ * @type {'fuzzy'|'strict'}
150
+ */
151
+ export let search_strategy = "fuzzy";
152
+
153
+ /**
154
+ * Whether to show the highlighted or plain results in the dropdown.
155
+ */
156
+ export let highlighted_results = true;
157
+ /**
158
+ * Size of the input
159
+ * @type {''|'small'|'medium'|'large'}
160
+ */
161
+ export let size = "";
162
+ /**
163
+ * Color of the input
164
+ * @type {''|'primary'|'success'|'warning'|'info'|'danger'|'dark'|'light'}
165
+ */
166
+ export let color = "";
167
+ /**
168
+ * Inline CSS for input container
169
+ */
170
+ export let style = "";
171
+ /**
172
+ * Marks component as read-only
173
+ */
174
+ export let readonly = false;
175
+ /**
176
+ * Disables the component
177
+ */
178
+ export let disabled = false;
179
+
180
+ /**
181
+ * Where to render the dropdown list.
182
+ * Can be a DOM element or a `string` with the CSS selector of the element.
183
+ *
184
+ * By default it renders in a custom container appended to `document.body`.
185
+ *
186
+ * @type { HTMLElement|string}
187
+ */
188
+ export let dropdown_portal = "#" + rootContainerId;
189
+
190
+ /**
191
+ * CSS classes for input container
192
+ */
193
+ let klass = "";
194
+ export { klass as class };
195
+
196
+ if (!search && (!options || !options.length))
197
+ console.error(`Missing options`);
198
+
199
+ //ensure we have a root container for all our hoisitng related stuff
200
+
201
+ let rootContainer = document.getElementById(rootContainerId);
202
+ if (!rootContainer) {
203
+ rootContainer = document.createElement("div");
204
+ rootContainer.id = rootContainerId;
205
+ document.body.appendChild(rootContainer);
206
+ }
207
+
208
+ const fire = createEventDispatcher();
209
+
210
+ let el, //whole wrapping element
211
+ dropdown, //dropdown ul
212
+ input, //the textbox to type in
213
+ POPPER,
214
+ active_option = "",
215
+ searching = true,
216
+ show_options = false,
217
+ filtered_options = [], //list of options filtered by search query
218
+ normalised_options = [], //list of options normalised
219
+ options_loading = false, //indictaes whether async search function is running
220
+ mounted = false; //indicates whether component is mounted
221
+
222
+ let list_text_size = {
223
+ small: "7",
224
+ medium: "5",
225
+ large: "4",
226
+ };
227
+
228
+ $: asyncMode = search && typeof search === "function";
229
+
230
+ $: options, normaliseOptions();
231
+ $: searching, updateFilteredOptions(value);
232
+
233
+ $: allow_fuzzy_match = !search && search_strategy === "fuzzy";
234
+
235
+ //convert arrays of strings into normalised arrays of objects
236
+ function normaliseOptions() {
237
+ let _items = options || [];
238
+ if (!_items || !(_items instanceof Array)) {
239
+ normalised_options = [];
240
+ return;
241
+ }
242
+
243
+ normalised_options = normaliseArraysToObjects(_items);
244
+ }
245
+
246
+ function updateFilteredOptions(value) {
247
+ if (!mounted) return;
248
+
249
+ if (asyncMode) {
250
+ searching && debouncedTriggerSearch(sanitizeFilters(value));
251
+ } else {
252
+ searching && triggerSearch(sanitizeFilters(value));
253
+ }
254
+ }
255
+
256
+ function triggerSearch(filters) {
257
+ let cache = {};
258
+ //TODO - can optimize more for very long lists
259
+ filters.forEach((word, idx) => {
260
+ // iterate over each word in the search query
261
+ let opts = [];
262
+ if (word) {
263
+ opts = [...normalised_options].filter((item) => {
264
+ // filter out items that don't match `filter`
265
+ if (typeof item === "object" && item.value) {
266
+ return typeof item.value === "string" && match(word, item.value);
267
+ }
268
+ });
269
+ }
270
+
271
+ cache[idx] = opts; // storing options to current index on cache
272
+ });
273
+
274
+ filtered_options = Object.values(cache) // get values from cache
275
+ .flat() // flatten array
276
+ .filter((v, i, self) => self.indexOf(v) === i); // remove duplicates
277
+
278
+ if (highlighted_results) {
279
+ filtered_options = highlightMatches(filtered_options, filters);
280
+ }
281
+ setOptionsVisible(true);
282
+ }
283
+
284
+ function triggerExternalSearch(filters) {
285
+ if (!filters.length) {
286
+ //do not trigger async search if filters are empty
287
+ clearDropDownResults();
288
+ return;
289
+ }
290
+ options_loading = true;
291
+ // filtered_options = [];
292
+ search(filters).then((_options) => {
293
+ searching = false;
294
+ options_loading = false;
295
+ tick().then(() => {
296
+ filtered_options = normaliseArraysToObjects(_options);
297
+
298
+ if (highlighted_results) {
299
+ filtered_options = highlightMatches(filtered_options, filters);
300
+ }
301
+ setOptionsVisible(true);
302
+ });
303
+ });
304
+ }
305
+
306
+ const debouncedTriggerSearch = debounce(triggerExternalSearch, 150, false);
307
+
308
+ onMount(() => {
309
+ POPPER = createPopper(el, dropdown, {
310
+ strategy: "fixed",
311
+ placement: "bottom-start",
312
+ modifiers: [sameWidthPopperModifier],
313
+ });
314
+
315
+ //normalize value
316
+ if (value === null || typeof value == "undefined") {
317
+ value = null;
318
+ }
319
+ mounted = true;
320
+
321
+ return () => {
322
+ POPPER.destroy();
323
+ };
324
+ });
325
+
326
+ function setOptionsVisible(show) {
327
+ if (readonly || disabled || show === show_options) return;
328
+ if (!value || !filtered_options.length) {
329
+ show = false;
330
+ }
331
+ show_options = show;
332
+ if (!show) {
333
+ clearDropDownResults();
334
+ }
335
+ POPPER && POPPER.update();
336
+ }
337
+
338
+ function handleKeydown(event) {
339
+ if (event.key === `Enter`) {
340
+ show_options && event.preventDefault();
341
+
342
+ if (active_option) {
343
+ handleOptionMouseDown(active_option);
344
+ } else {
345
+ // no active option means no option is selected and the actual value should be what typed in input.
346
+ setOptionsVisible(false);
347
+ }
348
+ } else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
349
+ show_options && event.preventDefault();
350
+
351
+ const increment = event.key === `ArrowUp` ? -1 : 1;
352
+ const newActiveIdx = filtered_options.indexOf(active_option) + increment;
353
+
354
+ if (newActiveIdx < 0) {
355
+ active_option = filtered_options[filtered_options.length - 1];
356
+ } else {
357
+ if (newActiveIdx === filtered_options.length)
358
+ active_option = filtered_options[0];
359
+ else active_option = filtered_options[newActiveIdx];
360
+ }
361
+ } else {
362
+ active_option = "";
363
+ searching = true;
364
+ }
365
+ }
366
+
367
+ function handleOptionMouseDown(option) {
368
+ add(option);
369
+ }
370
+
371
+ function add(token) {
372
+ if (readonly || disabled) {
373
+ return;
374
+ }
375
+
376
+ value = token.value;
377
+ fire("change", { token, type: `add` });
378
+
379
+ setOptionsVisible(false);
380
+ }
381
+
382
+ function blurEvent() {
383
+ /**
384
+ * Triggered when the input loses focus
385
+ */
386
+ fire("blur");
387
+ }
388
+
389
+ const match = (needle, haystack) => {
390
+ let _hayStack = haystack.toLowerCase();
391
+ return allow_fuzzy_match
392
+ ? fuzzysearch(needle, _hayStack)
393
+ : _hayStack.indexOf(needle) > -1;
394
+ };
395
+
396
+ const normaliseArraysToObjects = (arr) =>
397
+ [...arr].map((item) =>
398
+ typeof item === "object" ? item : { label: item, value: item }
399
+ );
400
+
401
+ const highlightMatches = (options, filters) => {
402
+ if (!filters.length) return options;
403
+ // join all filter parts and split into chars and filter out duplicates
404
+ let common_chars = [...filters.join("")].filter(
405
+ (v, i, self) => self.indexOf(v) === i
406
+ );
407
+ let pattern = new RegExp(`[${common_chars.join("")}]`, "gi");
408
+ return options.map((item) => {
409
+ return {
410
+ ...item,
411
+ label: item.value.replace(
412
+ pattern,
413
+ (match) => `<span class="h">${match}</span>`
414
+ ),
415
+ };
416
+ });
417
+ };
418
+
419
+ const clearDropDownResults = () => {
420
+ filtered_options = [];
421
+ searching = false;
422
+ };
423
+ function sanitizeFilters(v) {
424
+ return v && v.trim() ? v.toLowerCase().trim().split(/\s+/) : [];
425
+ }
426
+ </script>
@@ -131,6 +131,7 @@ Default value: `<span>{option[search_key] || option}</span>`
131
131
  on:mouseenter|preventDefault|stopPropagation={() => {
132
132
  activeOption = option;
133
133
  }}
134
+ class="is-size-{list_text_size[size]}"
134
135
  class:selected={isSelected(option)}
135
136
  class:active={activeOption === option}>
136
137
  <span class="kws-selected-icon"
@@ -145,7 +146,7 @@ Default value: `<span>{option[search_key] || option}</span>`
145
146
  </li>
146
147
  {:else}
147
148
  {#if !options_loading}
148
- <li class="no-options">
149
+ <li class="no-options is-size-{list_text_size[size]}">
149
150
  {searchText ? no_options_msg : async_search_prompt}
150
151
  </li>
151
152
  {/if}
@@ -160,7 +161,7 @@ Default value: `<span>{option[search_key] || option}</span>`
160
161
  import { debounce } from "@kws3/ui/utils";
161
162
  import { createEventDispatcher, onMount, tick } from "svelte";
162
163
  import { createPopper } from "@popperjs/core";
163
- import fuzzysearch from "fuzzysearch";
164
+ import fuzzysearch from "@kws3/ui/utils/fuzzysearch";
164
165
 
165
166
  const sameWidthPopperModifier = {
166
167
  name: "sameWidth",
@@ -322,6 +323,12 @@ Default value: `<span>{option[search_key] || option}</span>`
322
323
  selectedOptions = [], //list of options that are selected
323
324
  options_loading = false; //indictaes whether async search function is running
324
325
 
326
+ let list_text_size = {
327
+ small: "7",
328
+ medium: "5",
329
+ large: "4",
330
+ };
331
+
325
332
  $: single = max === 1;
326
333
  $: asyncMode = search && typeof search === "function";
327
334
  $: hasValue = single
@@ -603,6 +610,7 @@ Default value: `<span>{option[search_key] || option}</span>`
603
610
  setOptionsVisible(true);
604
611
  }
605
612
  } else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
613
+ showOptions && event.preventDefault();
606
614
  const increment = event.key === `ArrowUp` ? -1 : 1;
607
615
  const newActiveIdx = filteredOptions.indexOf(activeOption) + increment;
608
616
 
package/index.js CHANGED
@@ -49,6 +49,7 @@ export { default as Transition } from "./transitions/Transition.svelte";
49
49
  export { default as SlidingPane } from "./sliding-panes/SlidingPane.svelte";
50
50
  export { default as SlidingPaneSet } from "./sliding-panes/SlidingPaneSet.svelte";
51
51
 
52
+ export { default as AutoComplete } from "./forms/AutoComplete.svelte";
52
53
  export { default as SearchableSelect } from "./forms/select/SearchableSelect.svelte";
53
54
  export { default as MultiSelect } from "./forms/select/MultiSelect.svelte";
54
55
  export { default as MaskedInput } from "./forms/MaskedInput.svelte";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kws3/ui",
3
- "version": "1.8.1",
3
+ "version": "1.8.4",
4
4
  "description": "UI components for use with Svelte v3 applications.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -25,10 +25,9 @@
25
25
  "dependencies": {
26
26
  "apexcharts": "3.33.2",
27
27
  "flatpickr": "^4.5.2",
28
- "fuzzysearch": "^1.0.3",
29
28
  "svelte-portal": "^2.1.2",
30
29
  "text-mask-core": "^5.1.2",
31
30
  "tippy.js": "^6.3.1"
32
31
  },
33
- "gitHead": "2fdaba3e13397494f06b5881bf92f68b1c7c695d"
32
+ "gitHead": "440bdaf5e02a0a7ab0b69c2122c2c56da3485c1f"
34
33
  }
@@ -0,0 +1,132 @@
1
+ $kws-theme-colors: $colors !default;
2
+ $kws-autocomplete-radius: $radius !default;
3
+ $kws-autocomplete-border-color: $input-border-color !default;
4
+ $kws-autocomplete-box-shadow: 0 0.5em 1em -0.125em rgb(10 10 10 / 10%),
5
+ 0 0px 0 1px rgb(10 10 10 / 2%) !default;
6
+ $kws-autocomplete-focus-border-color: $input-focus-border-color !default;
7
+ $kws-autocomplete-focus-box-shadow-size: $input-focus-box-shadow-size !default;
8
+ $kws-autocomplete-focus-box-shadow-color: $input-focus-box-shadow-color !default;
9
+ $kws-autocomplete-disabled-background-color: $input-disabled-background-color !default;
10
+ $kws-autocomplete-disabled-border-color: $input-disabled-border-color !default;
11
+ $kws-autocomplete-disabled-color: $input-disabled-color !default;
12
+ $kws-autocomplete-selecting-color: $primary-invert !default;
13
+ $kws-autocomplete-selecting-background: $primary !default;
14
+ $kws-autocomplete-text-matches-color: currentColor !default;
15
+ $kws-autocomplete-text-matches-background: transparent !default;
16
+ $kws-autocomplete-text-matches-font-weight: $weight-bold !default;
17
+
18
+ $__modal-z: 41 !default;
19
+ @if $modal-z {
20
+ $__modal-z: $modal-z;
21
+ }
22
+
23
+ $kws-autocomplete-options-z-index: $__modal-z + 1 !default;
24
+
25
+ .kws-autocomplete {
26
+ position: relative;
27
+ align-items: center;
28
+ display: flex;
29
+ cursor: text;
30
+ height: auto;
31
+ min-height: 2.5em;
32
+ padding-top: calc(0.4em - 1px);
33
+ padding-bottom: calc(0.4em - 1px);
34
+ &:focus-within {
35
+ border-color: $kws-autocomplete-focus-border-color;
36
+ box-shadow: $kws-autocomplete-focus-box-shadow-size
37
+ $kws-autocomplete-focus-box-shadow-color;
38
+ }
39
+ &.is-disabled {
40
+ background-color: $kws-autocomplete-disabled-background-color;
41
+ border-color: $kws-autocomplete-disabled-border-color;
42
+ color: $kws-autocomplete-disabled-color;
43
+ cursor: not-allowed;
44
+ }
45
+ &.is-readonly {
46
+ box-shadow: none;
47
+ }
48
+
49
+ input {
50
+ border: none !important;
51
+ outline: none !important;
52
+ background: none !important;
53
+ /* needed to hide red shadow around required inputs in some browsers */
54
+ box-shadow: none !important;
55
+ color: inherit;
56
+ flex: 1;
57
+ padding: 1pt;
58
+ height: auto;
59
+ min-height: 0;
60
+ }
61
+
62
+ ul.options {
63
+ list-style: none;
64
+ max-height: 50vh;
65
+ padding: 0;
66
+ cursor: pointer;
67
+ overflow: auto;
68
+ background: #fff;
69
+ border: 1px solid $kws-autocomplete-border-color;
70
+ box-shadow: $kws-autocomplete-box-shadow;
71
+ position: relative;
72
+ z-index: 4;
73
+ &[data-popper-placement="top"] {
74
+ border-radius: $kws-autocomplete-radius $kws-autocomplete-radius 0 0;
75
+ box-shadow: 0 -1px 6px rgba(0, 0, 0, 0.4);
76
+ }
77
+ &.hidden {
78
+ display: none;
79
+ }
80
+
81
+ li {
82
+ padding: 0.3em 0.5em;
83
+ position: relative;
84
+ word-break: break-all;
85
+ &.active {
86
+ // keyboard focused item
87
+ color: $kws-autocomplete-selecting-color;
88
+ background: $kws-autocomplete-selecting-background;
89
+ }
90
+ span.h {
91
+ // highlight text matches
92
+ font-weight: $kws-autocomplete-text-matches-font-weight;
93
+ color: $kws-autocomplete-text-matches-color;
94
+ background: $kws-autocomplete-text-matches-background;
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ @each $name, $pair in $kws-theme-colors {
101
+ $color: nth($pair, 1);
102
+ $color-invert: nth($pair, 2);
103
+ $color-light: findLightColor($color);
104
+ $color-dark: findDarkColor($color);
105
+ .kws-autocomplete {
106
+ &.is-#{$name} {
107
+ border-color: $color;
108
+ &:focus-within {
109
+ box-shadow: $input-focus-box-shadow-size bulmaRgba($color, 0.25);
110
+ }
111
+ ul.options {
112
+ li {
113
+ &.selected {
114
+ color: $color-dark;
115
+ background: $color-light;
116
+ }
117
+ &.active {
118
+ color: $color-invert;
119
+ background: $color;
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ #kws-overlay-root {
128
+ .kws-autocomplete {
129
+ position: absolute;
130
+ z-index: $kws-autocomplete-options-z-index;
131
+ }
132
+ }
package/styles/Grid.scss CHANGED
@@ -51,6 +51,7 @@ $kws-gridview-checked-row-background: $primary-light !default;
51
51
  background-color: $kws-gridview-checked-row-background !important;
52
52
  td {
53
53
  background-color: $kws-gridview-checked-row-background !important;
54
+ color: findColorInvert($kws-gridview-checked-row-background) !important;
54
55
  }
55
56
  }
56
57
  }
@@ -0,0 +1,20 @@
1
+ export default function fuzzysearch(needle, haystack) {
2
+ var tlen = haystack.length;
3
+ var qlen = needle.length;
4
+ if (qlen > tlen) {
5
+ return false;
6
+ }
7
+ if (qlen === tlen) {
8
+ return needle === haystack;
9
+ }
10
+ outer: for (var i = 0, j = 0; i < qlen; i++) {
11
+ var nch = needle.charCodeAt(i);
12
+ while (j < tlen) {
13
+ if (haystack.charCodeAt(j++) === nch) {
14
+ continue outer;
15
+ }
16
+ }
17
+ return false;
18
+ }
19
+ return true;
20
+ }