@kws3/ui 1.7.4 → 1.8.2
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 +15 -0
- package/buttons/ConfirmButton.svelte +0 -2
- package/controls/NumberInput.svelte +93 -43
- package/forms/select/MultiSelect.svelte +201 -80
- package/forms/select/SearchableSelect.svelte +29 -0
- package/helpers/ActionSheet.svelte +3 -1
- package/helpers/CardModal.svelte +11 -1
- package/helpers/Dialog/index.js +2 -0
- package/helpers/Modal.svelte +12 -3
- package/helpers/ScrollableList.svelte +231 -0
- package/index.js +1 -0
- package/package.json +2 -2
- package/sliding-panes/SlidingPane.svelte +13 -20
- package/styles/ActionSheet.scss +10 -0
- package/utils/fuzzysearch.js +20 -0
- package/utils/resizeObserver.js +24 -0
package/CHANGELOG.mdx
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## 1.8.2
|
|
2
|
+
- Usability fixes for `NumberInput`
|
|
3
|
+
- New `input_only`, `force_integer`, `style` and `class` props for `NumberInput`
|
|
4
|
+
- Forward `focus`, `blur` input events for `NumberInput`
|
|
5
|
+
- Use custom version of `fuzzysearch` for `SearchableSelect` and `MultiSelect`
|
|
6
|
+
|
|
7
|
+
## 1.8.1
|
|
8
|
+
- New `ScrollableList` component
|
|
9
|
+
|
|
10
|
+
## 1.8.0
|
|
11
|
+
- `Modal`, `CardModal` and `ActionSheet` components now play an outro transition instead of abruptly disappearing.
|
|
12
|
+
- Usability fixes for `SearchableSelect` and `MultiSelect`.
|
|
13
|
+
- `SearchableSelect` and `MultiSelect` now support loading options via an async function.
|
|
14
|
+
- `SearchableSelect` and `MultiSelect` now match results using a fuzzy `search_strategy`. This can be changed to old behaviour by specifying `search_strategy="strict"`.
|
|
15
|
+
|
|
1
16
|
## 1.7.4
|
|
2
17
|
- Update ApexCharts to version 3.33.2
|
|
3
18
|
- Added support for subscribing to chart events
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
<p class="control">
|
|
29
29
|
{#if _confirm}
|
|
30
30
|
<button
|
|
31
|
-
role="button"
|
|
32
31
|
class="button is-success is-light is-shadowless is-{size} {button_class}"
|
|
33
32
|
type="button"
|
|
34
33
|
on:click|preventDefault|stopPropagation={cancel}>
|
|
@@ -38,7 +37,6 @@
|
|
|
38
37
|
</p>
|
|
39
38
|
<p class="control is-expanded">
|
|
40
39
|
<button
|
|
41
|
-
role="button"
|
|
42
40
|
class="button is-{size} {_doing
|
|
43
41
|
? main_color + ' is-loading'
|
|
44
42
|
: _error
|
|
@@ -20,54 +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
|
-
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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>
|
|
42
99
|
</div>
|
|
43
|
-
|
|
44
|
-
<input
|
|
45
|
-
data-testid="input"
|
|
46
|
-
class="input has-text-centered is-{size} is-{value < min || value > max
|
|
47
|
-
? 'danger'
|
|
48
|
-
: ''}"
|
|
49
|
-
type="number"
|
|
50
|
-
min
|
|
51
|
-
max
|
|
52
|
-
step
|
|
53
|
-
{disabled}
|
|
54
|
-
readonly={!typeable}
|
|
55
|
-
bind:value
|
|
56
|
-
on:blur={isBlurred()}
|
|
57
|
-
on:focus={isFocused()} />
|
|
58
|
-
</div>
|
|
59
|
-
<div class="control">
|
|
60
|
-
<button
|
|
61
|
-
role="button"
|
|
62
|
-
type="button"
|
|
63
|
-
class="button is-{size} is-{plus_button_color}"
|
|
64
|
-
style="box-shadow:none;"
|
|
65
|
-
on:click|preventDefault={count(+1)}
|
|
66
|
-
disabled={disabled || value >= max}>
|
|
67
|
-
<Icon icon={plus_icon} size="small" class="has-text-{plus_icon_color}" />
|
|
68
|
-
</button>
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
100
|
+
{/if}
|
|
71
101
|
|
|
72
102
|
<style>
|
|
73
103
|
input[type="number"]::-webkit-inner-spin-button,
|
|
@@ -160,7 +190,25 @@ This will be overridden if `min` is higher, or `max` is lower, Default: `0`
|
|
|
160
190
|
* Color of the Plus Button
|
|
161
191
|
* @type {''|'success'|'primary'|'warning'|'info'|'danger'|'dark'|'light'}
|
|
162
192
|
*/
|
|
163
|
-
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 };
|
|
164
212
|
|
|
165
213
|
let _has_focus = false,
|
|
166
214
|
_old_value = null;
|
|
@@ -191,6 +239,8 @@ This will be overridden if `min` is higher, or `max` is lower, Default: `0`
|
|
|
191
239
|
|
|
192
240
|
if (typeof value == "undefined" || value === null) value = min;
|
|
193
241
|
|
|
242
|
+
if (force_integer) value = Math.floor(Number(value));
|
|
243
|
+
|
|
194
244
|
if (value < min) value = min;
|
|
195
245
|
if (value > max) value = max;
|
|
196
246
|
|
|
@@ -16,6 +16,12 @@ Used to populate the list of options in the dropdown, Default: `[]`
|
|
|
16
16
|
this property of each object will be searched, Default: `"name"`
|
|
17
17
|
@param {string} [value_key="id"] - If `options` is an array of objects,
|
|
18
18
|
this property of each object will be returned as the value, Default: `"id"`
|
|
19
|
+
@param {function|null} [search=null] - Async function to fetch options
|
|
20
|
+
|
|
21
|
+
Only send this prop if you want to fetch `options` asynchronously.
|
|
22
|
+
`options` prop will be ignored if this prop is set., Default: `null`
|
|
23
|
+
@param {'fuzzy'|'strict'} [search_strategy="fuzzy"] - Filtered options to be displayed strictly based on search text or perform a fuzzy match.
|
|
24
|
+
Fuzzy match will not work if `search` function is set, as the backend service is meant to do the matching., Default: `"fuzzy"`
|
|
19
25
|
@param {''|'small'|'medium'|'large'} [size=""] - Size of the input, Default: `""`
|
|
20
26
|
@param {''|'primary'|'success'|'warning'|'info'|'danger'|'dark'|'light'} [color=""] - Color of the input, Default: `""`
|
|
21
27
|
@param {string} [style=""] - Inline CSS for input container, Default: `""`
|
|
@@ -24,6 +30,7 @@ this property of each object will be returned as the value, Default: `"id"`
|
|
|
24
30
|
@param {string} [selected_icon="check"] - Icon used to mark selected items in dropdown list, Default: `"check"`
|
|
25
31
|
@param {boolean} [summary_mode=false] - Shows only the number of items selected, instead of listing all the selected items in the input., Default: `false`
|
|
26
32
|
@param {string} [no_options_msg="No matching options"] - Message to display when no matching options are found, Default: `"No matching options"`
|
|
33
|
+
@param {string} [async_search_prompt="Start typing to search..."] - Message to display in dropdown when async search can be performed, Default: `"Start typing to search..."`
|
|
27
34
|
@param {string} [remove_btn_tip="Remove"] - Tooltip text for Remove Item button. This `string` will precede the selected Item Name in the tooltip., Default: `"Remove"`
|
|
28
35
|
@param {string} [remove_all_tip="Remove all"] - Tooltip text for the Clear All button, Default: `"Remove all"`
|
|
29
36
|
@param {HTMLElement|string} [dropdown_portal=undefined] - Where to render the dropdown list.
|
|
@@ -75,7 +82,6 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
75
82
|
{#if !readonly && !disabled}
|
|
76
83
|
<button
|
|
77
84
|
on:click|self|stopPropagation={() => remove(tag)}
|
|
78
|
-
role="button"
|
|
79
85
|
type="button"
|
|
80
86
|
class="delete is-small"
|
|
81
87
|
data-tooltip="{remove_btn_tip} {tag[used_search_key]}" />
|
|
@@ -84,6 +90,7 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
84
90
|
{/each}
|
|
85
91
|
{/if}
|
|
86
92
|
{/if}
|
|
93
|
+
{#if single}<span>{singleVisibleValue}</span>{/if}
|
|
87
94
|
<input
|
|
88
95
|
class="input is-{size}"
|
|
89
96
|
bind:this={input}
|
|
@@ -98,9 +105,13 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
98
105
|
on:blur={() => setOptionsVisible(false)}
|
|
99
106
|
placeholder={_placeholder} />
|
|
100
107
|
</ul>
|
|
101
|
-
{#if
|
|
108
|
+
{#if search && options_loading}
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
style="border: none;"
|
|
112
|
+
class="button is-paddingless delete is-medium is-loading" />
|
|
113
|
+
{:else if !readonly && !disabled}
|
|
102
114
|
<button
|
|
103
|
-
role="button"
|
|
104
115
|
type="button"
|
|
105
116
|
class="remove-all delete is-small"
|
|
106
117
|
data-tooltip={remove_all_tip}
|
|
@@ -133,7 +144,11 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
133
144
|
{option}>{option[used_search_key] || option}</slot>
|
|
134
145
|
</li>
|
|
135
146
|
{:else}
|
|
136
|
-
|
|
147
|
+
{#if !options_loading}
|
|
148
|
+
<li class="no-options">
|
|
149
|
+
{searchText ? no_options_msg : async_search_prompt}
|
|
150
|
+
</li>
|
|
151
|
+
{/if}
|
|
137
152
|
{/each}
|
|
138
153
|
</ul>
|
|
139
154
|
</div>
|
|
@@ -142,8 +157,10 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
142
157
|
|
|
143
158
|
<script>
|
|
144
159
|
import { Icon, portal } from "@kws3/ui";
|
|
145
|
-
import {
|
|
160
|
+
import { debounce } from "@kws3/ui/utils";
|
|
161
|
+
import { createEventDispatcher, onMount, tick } from "svelte";
|
|
146
162
|
import { createPopper } from "@popperjs/core";
|
|
163
|
+
import fuzzysearch from "@kws3/ui/utils/fuzzysearch";
|
|
147
164
|
|
|
148
165
|
const sameWidthPopperModifier = {
|
|
149
166
|
name: "sameWidth",
|
|
@@ -196,6 +213,22 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
196
213
|
* this property of each object will be returned as the value
|
|
197
214
|
*/
|
|
198
215
|
export let value_key = "id";
|
|
216
|
+
/**
|
|
217
|
+
* Async function to fetch options
|
|
218
|
+
*
|
|
219
|
+
* Only send this prop if you want to fetch `options` asynchronously.
|
|
220
|
+
* `options` prop will be ignored if this prop is set.
|
|
221
|
+
*
|
|
222
|
+
* @type {function|null}
|
|
223
|
+
*/
|
|
224
|
+
export let search = null;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Filtered options to be displayed strictly based on search text or perform a fuzzy match.
|
|
228
|
+
* Fuzzy match will not work if `search` function is set, as the backend service is meant to do the matching.
|
|
229
|
+
* @type {'fuzzy'|'strict'}
|
|
230
|
+
*/
|
|
231
|
+
export let search_strategy = "fuzzy";
|
|
199
232
|
/**
|
|
200
233
|
* Size of the input
|
|
201
234
|
* @type {''|'small'|'medium'|'large'}
|
|
@@ -230,6 +263,10 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
230
263
|
* Message to display when no matching options are found
|
|
231
264
|
*/
|
|
232
265
|
export let no_options_msg = "No matching options";
|
|
266
|
+
/**
|
|
267
|
+
* Message to display in dropdown when async search can be performed
|
|
268
|
+
*/
|
|
269
|
+
export let async_search_prompt = "Start typing to search...";
|
|
233
270
|
/**
|
|
234
271
|
* Tooltip text for Remove Item button. This `string` will precede the selected Item Name in the tooltip.
|
|
235
272
|
* */
|
|
@@ -254,7 +291,8 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
254
291
|
let klass = "";
|
|
255
292
|
export { klass as class };
|
|
256
293
|
|
|
257
|
-
if (!options || !options.length)
|
|
294
|
+
if (!search && (!options || !options.length))
|
|
295
|
+
console.error(`Missing options`);
|
|
258
296
|
|
|
259
297
|
if (max !== null && max < 0) {
|
|
260
298
|
throw new TypeError(`max must be null or positive integer, got ${max}`);
|
|
@@ -281,9 +319,11 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
281
319
|
showOptions = false,
|
|
282
320
|
filteredOptions = [], //list of options filtered by search query
|
|
283
321
|
normalisedOptions = [], //list of options normalised
|
|
284
|
-
selectedOptions = []
|
|
322
|
+
selectedOptions = [], //list of options that are selected
|
|
323
|
+
options_loading = false; //indictaes whether async search function is running
|
|
285
324
|
|
|
286
325
|
$: single = max === 1;
|
|
326
|
+
$: asyncMode = search && typeof search === "function";
|
|
287
327
|
$: hasValue = single
|
|
288
328
|
? value !== null && typeof value != "undefined"
|
|
289
329
|
? true
|
|
@@ -324,6 +364,13 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
324
364
|
else return value.some((v) => matchesValue(v, option));
|
|
325
365
|
};
|
|
326
366
|
|
|
367
|
+
$: singleVisibleValue =
|
|
368
|
+
!searching && single && hasValue && selectedOptions && selectedOptions[0]
|
|
369
|
+
? selectedOptions[0][used_search_key]
|
|
370
|
+
: "";
|
|
371
|
+
|
|
372
|
+
$: allow_fuzzy_match = !search && search_strategy === "fuzzy";
|
|
373
|
+
|
|
327
374
|
//convert arrays of strings into normalised arrays of objects
|
|
328
375
|
function normaliseOptions() {
|
|
329
376
|
let _items = options || [];
|
|
@@ -332,15 +379,7 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
332
379
|
return;
|
|
333
380
|
}
|
|
334
381
|
|
|
335
|
-
normalisedOptions = _items
|
|
336
|
-
if (typeof item === "object") {
|
|
337
|
-
return item;
|
|
338
|
-
}
|
|
339
|
-
let __obj = {};
|
|
340
|
-
__obj[used_search_key] = item;
|
|
341
|
-
__obj[used_value_key] = item;
|
|
342
|
-
return __obj;
|
|
343
|
-
});
|
|
382
|
+
normalisedOptions = normaliseArraysToObjects(_items);
|
|
344
383
|
}
|
|
345
384
|
|
|
346
385
|
function updateFilteredOptions() {
|
|
@@ -353,29 +392,27 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
353
392
|
} else {
|
|
354
393
|
filter = searchText.toLowerCase();
|
|
355
394
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
typeof item[key] === "string" &&
|
|
370
|
-
|
|
371
|
-
)
|
|
372
|
-
return true;
|
|
395
|
+
if (asyncMode && searching) {
|
|
396
|
+
debouncedTriggerSearch(filter);
|
|
397
|
+
} else {
|
|
398
|
+
filteredOptions = normalisedOptions.slice().filter((item) => {
|
|
399
|
+
// filter out items that don't match `filter`
|
|
400
|
+
if (typeof item === "object") {
|
|
401
|
+
if (used_search_key) {
|
|
402
|
+
return (
|
|
403
|
+
typeof item[used_search_key] === "string" &&
|
|
404
|
+
match(filter, item[used_search_key])
|
|
405
|
+
);
|
|
406
|
+
} else {
|
|
407
|
+
for (var key in item) {
|
|
408
|
+
return typeof item[key] === "string" && match(filter, item[key]);
|
|
409
|
+
}
|
|
373
410
|
}
|
|
411
|
+
} else {
|
|
412
|
+
return match(filter, item);
|
|
374
413
|
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
});
|
|
414
|
+
});
|
|
415
|
+
}
|
|
379
416
|
}
|
|
380
417
|
|
|
381
418
|
function fillSelectedOptions() {
|
|
@@ -383,9 +420,17 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
383
420
|
selectedOptions = normalisedOptions.filter(
|
|
384
421
|
(v) => `${v[used_value_key]}` === `${value}`
|
|
385
422
|
);
|
|
386
|
-
setSingleVisibleValue();
|
|
387
423
|
} else {
|
|
388
|
-
|
|
424
|
+
let _normalisedOptions = asyncMode
|
|
425
|
+
? [...selectedOptions, ...normalisedOptions].filter(
|
|
426
|
+
//de-dupe by `used_value_key` when in asyncMode
|
|
427
|
+
(value, idx, self) =>
|
|
428
|
+
idx ===
|
|
429
|
+
self.findIndex((v) => v[used_value_key] === value[used_value_key])
|
|
430
|
+
)
|
|
431
|
+
: normalisedOptions;
|
|
432
|
+
|
|
433
|
+
selectedOptions = _normalisedOptions
|
|
389
434
|
.filter(
|
|
390
435
|
(v) => value && value.some((vl) => `${v[used_value_key]}` === `${vl}`)
|
|
391
436
|
)
|
|
@@ -398,6 +443,23 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
398
443
|
POPPER && POPPER.update();
|
|
399
444
|
}
|
|
400
445
|
|
|
446
|
+
function triggerSearch(filter) {
|
|
447
|
+
if (filter === "") {
|
|
448
|
+
//do not trigger async search if filter is empty
|
|
449
|
+
options = [];
|
|
450
|
+
searching = false;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
options_loading = true;
|
|
454
|
+
search(filter).then((_options) => {
|
|
455
|
+
options = _options;
|
|
456
|
+
searching = false;
|
|
457
|
+
options_loading = false;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const debouncedTriggerSearch = debounce(triggerSearch, 150, false);
|
|
462
|
+
|
|
401
463
|
onMount(() => {
|
|
402
464
|
POPPER = createPopper(el, dropdown, {
|
|
403
465
|
strategy: "fixed",
|
|
@@ -406,10 +468,25 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
406
468
|
});
|
|
407
469
|
|
|
408
470
|
//normalize value for single versus multiselect
|
|
409
|
-
if (value === null || typeof value == "undefined")
|
|
471
|
+
if (value === null || typeof value == "undefined") {
|
|
410
472
|
value = single ? null : [];
|
|
473
|
+
}
|
|
411
474
|
|
|
412
|
-
|
|
475
|
+
if (asyncMode) {
|
|
476
|
+
// initally on async mode options are empty
|
|
477
|
+
// so we need to fill selectedOptions with value if value is avaliable
|
|
478
|
+
options = value ? [...(single ? [value] : [...value])] : [];
|
|
479
|
+
searching = false;
|
|
480
|
+
tick().then(() => {
|
|
481
|
+
normaliseOptions();
|
|
482
|
+
value = normaliseArraysToObjects(options).map((v) => v[used_value_key]);
|
|
483
|
+
if (single && Array.isArray(value)) {
|
|
484
|
+
value = value[0];
|
|
485
|
+
}
|
|
486
|
+
fillSelectedOptions();
|
|
487
|
+
clearDropDownResults();
|
|
488
|
+
});
|
|
489
|
+
}
|
|
413
490
|
|
|
414
491
|
return () => {
|
|
415
492
|
POPPER.destroy();
|
|
@@ -424,24 +501,39 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
424
501
|
let isAlreadySelected = isSelected(token);
|
|
425
502
|
|
|
426
503
|
if (single) {
|
|
427
|
-
if (isAlreadySelected) {
|
|
428
|
-
setSingleVisibleValue();
|
|
429
|
-
} else {
|
|
504
|
+
if (!isAlreadySelected) {
|
|
430
505
|
value = token[used_value_key];
|
|
431
|
-
input && input.blur();
|
|
432
|
-
setOptionsVisible(false);
|
|
433
506
|
fire("change", { token, type: `add` });
|
|
507
|
+
//clear dropdown results in asyncMode
|
|
508
|
+
if (asyncMode) {
|
|
509
|
+
clearDropDownResults();
|
|
510
|
+
}
|
|
434
511
|
}
|
|
512
|
+
setOptionsVisible(false);
|
|
435
513
|
}
|
|
436
514
|
|
|
437
515
|
if (!isAlreadySelected && !single && (max === null || value.length < max)) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
516
|
+
if (asyncMode) {
|
|
517
|
+
//Do not filter invalid options, as they are async and might not be invalid
|
|
518
|
+
//but ensure they are unique
|
|
519
|
+
value = [...value, token[used_value_key]].filter(
|
|
520
|
+
(v, i, a) => a.indexOf(v) === i
|
|
521
|
+
);
|
|
522
|
+
} else {
|
|
523
|
+
//attach to value array while filtering out invalid values
|
|
524
|
+
value = [...value, token[used_value_key]].filter((v) => {
|
|
525
|
+
return normalisedOptions.filter((nv) => nv[used_value_key] === v)
|
|
526
|
+
.length;
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
443
530
|
searchText = ""; // reset search string on selection
|
|
444
531
|
|
|
532
|
+
//clear dropdown results in asyncMode
|
|
533
|
+
if (asyncMode) {
|
|
534
|
+
clearDropDownResults();
|
|
535
|
+
}
|
|
536
|
+
|
|
445
537
|
if (value && value.length && value.length === max) {
|
|
446
538
|
input && input.blur();
|
|
447
539
|
setOptionsVisible(false);
|
|
@@ -460,6 +552,11 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
460
552
|
value = value.filter
|
|
461
553
|
? value.filter((item) => !matchesValue(item, token))
|
|
462
554
|
: value;
|
|
555
|
+
|
|
556
|
+
//clear dropdown results in asyncMode
|
|
557
|
+
if (asyncMode) {
|
|
558
|
+
clearDropDownResults();
|
|
559
|
+
}
|
|
463
560
|
/**
|
|
464
561
|
* Triggered when an item is removed from selected Items
|
|
465
562
|
*/
|
|
@@ -483,25 +580,16 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
483
580
|
showOptions = show;
|
|
484
581
|
if (show) {
|
|
485
582
|
input && input.focus();
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function setSingleVisibleValue() {
|
|
491
|
-
if (single && hasValue) {
|
|
492
|
-
searchText =
|
|
493
|
-
selectedOptions && selectedOptions[0]
|
|
494
|
-
? selectedOptions[0][used_search_key]
|
|
495
|
-
: "";
|
|
583
|
+
} else {
|
|
584
|
+
searchText = "";
|
|
496
585
|
searching = false;
|
|
497
586
|
}
|
|
587
|
+
POPPER && POPPER.update();
|
|
498
588
|
}
|
|
499
589
|
|
|
500
590
|
function handleKeydown(event) {
|
|
501
591
|
if (event.key === `Escape`) {
|
|
502
|
-
|
|
503
|
-
searchText = "";
|
|
504
|
-
}
|
|
592
|
+
searchText = "";
|
|
505
593
|
} else {
|
|
506
594
|
setOptionsVisible(true);
|
|
507
595
|
}
|
|
@@ -510,9 +598,6 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
510
598
|
event.preventDefault();
|
|
511
599
|
if (activeOption) {
|
|
512
600
|
handleOptionMouseDown(activeOption);
|
|
513
|
-
if (!single) {
|
|
514
|
-
searchText = "";
|
|
515
|
-
}
|
|
516
601
|
} else {
|
|
517
602
|
// no active option means the options are closed in which case enter means open
|
|
518
603
|
setOptionsVisible(true);
|
|
@@ -529,24 +614,31 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
529
614
|
else activeOption = filteredOptions[newActiveIdx];
|
|
530
615
|
}
|
|
531
616
|
} else if (event.key === `Backspace`) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
617
|
+
if (single && hasValue) {
|
|
618
|
+
//for a single select
|
|
619
|
+
//if a value is already selected, backspace will clear the value
|
|
620
|
+
value = null;
|
|
621
|
+
searchText = "";
|
|
622
|
+
} else if (!single && searchText.length === 0) {
|
|
623
|
+
//for a multi select
|
|
624
|
+
// only remove selected tags on backspace if there are any and no searchText characters remain
|
|
625
|
+
if (value && value.length > 0) {
|
|
626
|
+
value = value.slice(0, value.length - 1);
|
|
542
627
|
}
|
|
543
628
|
} else {
|
|
544
|
-
|
|
545
|
-
searching = true;
|
|
546
|
-
}
|
|
629
|
+
searching = true;
|
|
547
630
|
}
|
|
548
631
|
} else {
|
|
632
|
+
//for a single select
|
|
633
|
+
//if a value is already selected,
|
|
634
|
+
//ignore keys other than navigation, enter and backspace
|
|
549
635
|
if (single) {
|
|
636
|
+
if (hasValue) {
|
|
637
|
+
event.preventDefault();
|
|
638
|
+
} else {
|
|
639
|
+
searching = true;
|
|
640
|
+
}
|
|
641
|
+
} else {
|
|
550
642
|
searching = true;
|
|
551
643
|
}
|
|
552
644
|
}
|
|
@@ -565,6 +657,9 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
565
657
|
fire("change", { token: value, type: `remove` });
|
|
566
658
|
value = single ? null : [];
|
|
567
659
|
searchText = "";
|
|
660
|
+
if (asyncMode) {
|
|
661
|
+
clearDropDownResults();
|
|
662
|
+
}
|
|
568
663
|
};
|
|
569
664
|
|
|
570
665
|
const matchesValue = (_value, _option) => {
|
|
@@ -575,4 +670,30 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
575
670
|
`${_value[used_value_key] || _value}` === `${_option[used_value_key]}`
|
|
576
671
|
);
|
|
577
672
|
};
|
|
673
|
+
|
|
674
|
+
const match = (needle, haystack) => {
|
|
675
|
+
let _hayStack = haystack.toLowerCase();
|
|
676
|
+
return allow_fuzzy_match
|
|
677
|
+
? fuzzysearch(needle, _hayStack)
|
|
678
|
+
: _hayStack.indexOf(needle) > -1;
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const normaliseArraysToObjects = (arr) => {
|
|
682
|
+
return arr.slice().map((item) => {
|
|
683
|
+
if (typeof item === "object") {
|
|
684
|
+
return item;
|
|
685
|
+
}
|
|
686
|
+
let __obj = {};
|
|
687
|
+
__obj[used_search_key] = item;
|
|
688
|
+
__obj[used_value_key] = item;
|
|
689
|
+
return __obj;
|
|
690
|
+
});
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const clearDropDownResults = () => {
|
|
694
|
+
tick().then(() => {
|
|
695
|
+
options = [];
|
|
696
|
+
searching = false;
|
|
697
|
+
});
|
|
698
|
+
};
|
|
578
699
|
</script>
|
|
@@ -16,9 +16,16 @@ this property of each object will be returned as the value, Default: `"id"`
|
|
|
16
16
|
@param {''|'primary'|'success'|'warning'|'info'|'danger'|'dark'|'light'} [color=""] - Color of the input, Default: `""`
|
|
17
17
|
@param {string} [style=""] - Inline CSS for input container, Default: `""`
|
|
18
18
|
@param {boolean} [readonly=false] - Marks component as read-only, Default: `false`
|
|
19
|
+
@param {function|null} [search=null] - Async function to fetch options
|
|
20
|
+
|
|
21
|
+
Only send this prop if you want to fetch `options` asynchronously.
|
|
22
|
+
`options` prop will be ignored if this prop is set., Default: `null`
|
|
23
|
+
@param {'fuzzy'|'strict'} [search_strategy="fuzzy"] - Filtered options to be displayed strictly based on search text or perform a fuzzy match.
|
|
24
|
+
Fuzzy match will not work if `search` function is set, as the backend service is meant to do the matching., Default: `"fuzzy"`
|
|
19
25
|
@param {boolean} [disabled=false] - Disables the component, Default: `false`
|
|
20
26
|
@param {string} [selected_icon="check"] - Icon used to mark selected items in dropdown list, Default: `"check"`
|
|
21
27
|
@param {string} [no_options_msg="No matching options"] - Message to display when no matching options are found, Default: `"No matching options"`
|
|
28
|
+
@param {string} [async_search_prompt="Start typing to search..."] - Message to display in dropdown when async search can be performed, Default: `"Start typing to search..."`
|
|
22
29
|
@param {string} [remove_all_tip="Clear Selection"] - Tooltip text for the Clear selection button, Default: `"Clear Selection"`
|
|
23
30
|
@param {HTMLElement|string} [dropdown_portal=undefined] - Where to render the dropdown list.
|
|
24
31
|
Can be a DOM element or a `string` with the CSS selector of the element.
|
|
@@ -44,13 +51,16 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
44
51
|
{options}
|
|
45
52
|
{search_key}
|
|
46
53
|
{value_key}
|
|
54
|
+
{search_strategy}
|
|
47
55
|
{size}
|
|
48
56
|
{color}
|
|
49
57
|
{style}
|
|
50
58
|
{readonly}
|
|
51
59
|
{disabled}
|
|
60
|
+
{search}
|
|
52
61
|
{selected_icon}
|
|
53
62
|
{remove_all_tip}
|
|
63
|
+
async_search_prompt={value ? "Backspace to clear" : async_search_prompt}
|
|
54
64
|
{no_options_msg}
|
|
55
65
|
{dropdown_portal}
|
|
56
66
|
on:change={change}
|
|
@@ -118,6 +128,21 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
118
128
|
* Marks component as read-only
|
|
119
129
|
*/
|
|
120
130
|
export let readonly = false;
|
|
131
|
+
/**
|
|
132
|
+
* Async function to fetch options
|
|
133
|
+
*
|
|
134
|
+
* Only send this prop if you want to fetch `options` asynchronously.
|
|
135
|
+
* `options` prop will be ignored if this prop is set.
|
|
136
|
+
*
|
|
137
|
+
* @type {function|null}
|
|
138
|
+
*/
|
|
139
|
+
export let search = null;
|
|
140
|
+
/**
|
|
141
|
+
* Filtered options to be displayed strictly based on search text or perform a fuzzy match.
|
|
142
|
+
* Fuzzy match will not work if `search` function is set, as the backend service is meant to do the matching.
|
|
143
|
+
* @type {'fuzzy'|'strict'}
|
|
144
|
+
*/
|
|
145
|
+
export let search_strategy = "fuzzy";
|
|
121
146
|
/**
|
|
122
147
|
* Disables the component
|
|
123
148
|
*/
|
|
@@ -130,6 +155,10 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
130
155
|
* Message to display when no matching options are found
|
|
131
156
|
*/
|
|
132
157
|
export let no_options_msg = "No matching options";
|
|
158
|
+
/**
|
|
159
|
+
* Message to display in dropdown when async search can be performed
|
|
160
|
+
*/
|
|
161
|
+
export let async_search_prompt = "Start typing to search...";
|
|
133
162
|
/**
|
|
134
163
|
* Tooltip text for the Clear selection button
|
|
135
164
|
*/
|
|
@@ -19,7 +19,9 @@ If `false` , the component won't have a close button, and will not close on clic
|
|
|
19
19
|
|
|
20
20
|
-->
|
|
21
21
|
|
|
22
|
-
<div
|
|
22
|
+
<div
|
|
23
|
+
class="modal kws-action-sheet-outer {klass} {is_active ? 'is-active' : ''}"
|
|
24
|
+
{style}>
|
|
23
25
|
{#if is_active}<div
|
|
24
26
|
transition:fade={{ duration: transitionDuration }}
|
|
25
27
|
class="modal-background"
|
package/helpers/CardModal.svelte
CHANGED
|
@@ -34,7 +34,6 @@ Only visible when the
|
|
|
34
34
|
transition:fade={{ duration: transitionDuration }}
|
|
35
35
|
class="modal-background"
|
|
36
36
|
on:click={clickOutside} />
|
|
37
|
-
|
|
38
37
|
<div
|
|
39
38
|
transition:scale={{
|
|
40
39
|
duration: transitionDuration,
|
|
@@ -83,6 +82,17 @@ Only visible when the
|
|
|
83
82
|
</div>
|
|
84
83
|
|
|
85
84
|
<style lang="scss">
|
|
85
|
+
.modal {
|
|
86
|
+
display: flex;
|
|
87
|
+
visibility: hidden;
|
|
88
|
+
&.is-active {
|
|
89
|
+
visibility: visible;
|
|
90
|
+
}
|
|
91
|
+
.modal-card,
|
|
92
|
+
.modal-background {
|
|
93
|
+
transition: all 0.3s;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
86
96
|
@media screen and (min-width: 769px), print {
|
|
87
97
|
.modal-card {
|
|
88
98
|
min-width: 640px;
|
package/helpers/Dialog/index.js
CHANGED
package/helpers/Modal.svelte
CHANGED
|
@@ -26,7 +26,6 @@ Only programmatic closing is possible, Default: `true`
|
|
|
26
26
|
transition:fade={{ duration: transitionDuration }}
|
|
27
27
|
class="modal-background"
|
|
28
28
|
on:click={clickOutside} />
|
|
29
|
-
|
|
30
29
|
<div
|
|
31
30
|
transition:scale={{
|
|
32
31
|
duration: transitionDuration,
|
|
@@ -47,9 +46,19 @@ Only programmatic closing is possible, Default: `true`
|
|
|
47
46
|
</div>
|
|
48
47
|
|
|
49
48
|
<style lang="scss">
|
|
50
|
-
|
|
49
|
+
.modal {
|
|
50
|
+
display: flex;
|
|
51
|
+
visibility: hidden;
|
|
52
|
+
&.is-active {
|
|
53
|
+
visibility: visible;
|
|
54
|
+
}
|
|
51
55
|
.modal-content,
|
|
52
|
-
.modal-
|
|
56
|
+
.modal-background {
|
|
57
|
+
transition: all 0.3s;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
@media screen and (min-width: 769px), print {
|
|
61
|
+
.modal-content {
|
|
53
62
|
min-width: 640px;
|
|
54
63
|
&.is-medium {
|
|
55
64
|
width: 70%;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@param {array} [items=[]] - Array of items, Default: `[]`
|
|
6
|
+
@param {string} [height="100%"] - Height of the wrapper, CSS String, Default: `"100%"`
|
|
7
|
+
@param {number | null} [item_height=null] - Height of each list item. If not set, height will be calculated automatically based on each item's offsetHeight, Default: `null`
|
|
8
|
+
@param {number} [start=0] - First item index rendered inside viewport - readonly, Default: `0`
|
|
9
|
+
@param {number} [end=0] - Last item index rendered inside viewport - readonly, Default: `0`
|
|
10
|
+
@param {number} [end_threshold=10] - `end` event will be fired when the list reaches this many items before the end of the list., Default: `10`
|
|
11
|
+
@param {string} [style=""] - Inline CSS for scroller container, Default: `""`
|
|
12
|
+
@param {string} [class=""] - CSS classes for scroller container, Default: `""`
|
|
13
|
+
|
|
14
|
+
### Events
|
|
15
|
+
- `end` - Fired when the list reaches `end_threshold` items before the end of the list.
|
|
16
|
+
|
|
17
|
+
### Slots
|
|
18
|
+
- `<slot name="default" {item} {index} />` - Default slot for list view items
|
|
19
|
+
- `<slot name="loader" />` - Optional slot to display a loading graphic at the bottom of the list
|
|
20
|
+
while more items are loading
|
|
21
|
+
|
|
22
|
+
-->
|
|
23
|
+
{#if hasResizeObserver}
|
|
24
|
+
<div
|
|
25
|
+
bind:this={viewport}
|
|
26
|
+
class="kws-scrollable-list with-resize-observer {klass}"
|
|
27
|
+
on:scroll={handle_scroll}
|
|
28
|
+
style="height:{height};{style}"
|
|
29
|
+
use:resizeObserver
|
|
30
|
+
on:resize={resize}>
|
|
31
|
+
<div
|
|
32
|
+
bind:this={contents}
|
|
33
|
+
style="padding-top: {top}px; padding-bottom: {bottom}px;">
|
|
34
|
+
{#each visible as item (item.index)}
|
|
35
|
+
<div class="row">
|
|
36
|
+
<!--Default slot for list view items-->
|
|
37
|
+
<slot item={item.data} index={item.index} />
|
|
38
|
+
</div>
|
|
39
|
+
{/each}
|
|
40
|
+
<!--Optional slot to display a loading graphic at the bottom of the list
|
|
41
|
+
while more items are loading-->
|
|
42
|
+
<slot name="loader" />
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
{:else}
|
|
46
|
+
<div
|
|
47
|
+
bind:this={viewport}
|
|
48
|
+
class="kws-scrollable-list {klass}"
|
|
49
|
+
on:scroll={handle_scroll}
|
|
50
|
+
style="height:{height};{style}"
|
|
51
|
+
bind:offsetHeight={viewport_height}>
|
|
52
|
+
<div
|
|
53
|
+
bind:this={contents}
|
|
54
|
+
style="padding-top: {top}px; padding-bottom: {bottom}px;">
|
|
55
|
+
{#each visible as item (item.index)}
|
|
56
|
+
<div class="row">
|
|
57
|
+
<!--Default slot for list view items-->
|
|
58
|
+
<slot item={item.data} index={item.index} />
|
|
59
|
+
</div>
|
|
60
|
+
{/each}
|
|
61
|
+
<!--Optional slot to display a loading graphic at the bottom of the list
|
|
62
|
+
while more items are loading-->
|
|
63
|
+
<slot name="loader" />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
{/if}
|
|
67
|
+
|
|
68
|
+
<style>
|
|
69
|
+
.kws-scrollable-list {
|
|
70
|
+
overflow: auto;
|
|
71
|
+
-webkit-overflow-scrolling: touch;
|
|
72
|
+
position: relative;
|
|
73
|
+
height: 100%;
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
76
|
+
|
|
77
|
+
<script>
|
|
78
|
+
import { onMount, tick } from "svelte";
|
|
79
|
+
import { createEventDispatcher } from "svelte";
|
|
80
|
+
import {
|
|
81
|
+
resizeObserver,
|
|
82
|
+
hasResizeObserver,
|
|
83
|
+
} from "@kws3/ui/utils/resizeObserver";
|
|
84
|
+
|
|
85
|
+
const fire = createEventDispatcher();
|
|
86
|
+
/**
|
|
87
|
+
* Array of items
|
|
88
|
+
*/
|
|
89
|
+
export let items = [],
|
|
90
|
+
/**
|
|
91
|
+
* Height of the wrapper, CSS String
|
|
92
|
+
*/
|
|
93
|
+
height = "100%",
|
|
94
|
+
/**
|
|
95
|
+
* Height of each list item. If not set, height will be calculated automatically based on each item's offsetHeight
|
|
96
|
+
* @type {number | null}
|
|
97
|
+
*/
|
|
98
|
+
item_height = null,
|
|
99
|
+
/**
|
|
100
|
+
* First item index rendered inside viewport - readonly
|
|
101
|
+
*/
|
|
102
|
+
start = 0,
|
|
103
|
+
/**
|
|
104
|
+
* Last item index rendered inside viewport - readonly
|
|
105
|
+
*/
|
|
106
|
+
end = 0,
|
|
107
|
+
/**
|
|
108
|
+
* `end` event will be fired when the list reaches this many items before the end of the list.
|
|
109
|
+
*/
|
|
110
|
+
end_threshold = 10,
|
|
111
|
+
/**
|
|
112
|
+
* Inline CSS for scroller container
|
|
113
|
+
*/
|
|
114
|
+
style = "";
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* CSS classes for scroller container
|
|
118
|
+
*/
|
|
119
|
+
let klass = "";
|
|
120
|
+
export { klass as class };
|
|
121
|
+
|
|
122
|
+
// local state
|
|
123
|
+
let height_map = [],
|
|
124
|
+
rows,
|
|
125
|
+
viewport,
|
|
126
|
+
contents,
|
|
127
|
+
viewport_height = 0,
|
|
128
|
+
visible,
|
|
129
|
+
mounted,
|
|
130
|
+
top = 0,
|
|
131
|
+
bottom = 0,
|
|
132
|
+
average_height,
|
|
133
|
+
items_count = 0;
|
|
134
|
+
|
|
135
|
+
$: visible = items.slice(start, end).map((data, i) => {
|
|
136
|
+
return { index: i + start, data };
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// whenever `items` changes, invalidate the current heightmap
|
|
140
|
+
$: items, viewport_height, item_height, mounted, refresh();
|
|
141
|
+
|
|
142
|
+
async function refresh() {
|
|
143
|
+
if (!mounted) return;
|
|
144
|
+
const scrollTop = viewport.scrollTop;
|
|
145
|
+
await tick(); // wait until the DOM is up to date
|
|
146
|
+
let content_height = top - scrollTop;
|
|
147
|
+
let i = start;
|
|
148
|
+
while (content_height < viewport_height && i < items.length) {
|
|
149
|
+
let row = rows[i - start];
|
|
150
|
+
if (!row) {
|
|
151
|
+
end = i + 1;
|
|
152
|
+
await tick(); // render the newly visible row
|
|
153
|
+
row = rows[i - start];
|
|
154
|
+
}
|
|
155
|
+
const row_height = (height_map[i] =
|
|
156
|
+
item_height || (row ? row.offsetHeight : 0));
|
|
157
|
+
content_height += row_height;
|
|
158
|
+
i += 1;
|
|
159
|
+
}
|
|
160
|
+
end = i;
|
|
161
|
+
const remaining = items.length - end;
|
|
162
|
+
average_height = (top + content_height) / end;
|
|
163
|
+
bottom = remaining * average_height;
|
|
164
|
+
height_map.length = items.length;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function handle_scroll() {
|
|
168
|
+
const scrollTop = viewport.scrollTop;
|
|
169
|
+
const old_start = start;
|
|
170
|
+
for (let v = 0; v < rows.length; v += 1) {
|
|
171
|
+
height_map[start + v] = item_height || rows[v].offsetHeight;
|
|
172
|
+
}
|
|
173
|
+
let i = 0;
|
|
174
|
+
let y = 0;
|
|
175
|
+
while (i < items.length) {
|
|
176
|
+
const row_height = height_map[i] || average_height;
|
|
177
|
+
if (y + row_height > scrollTop) {
|
|
178
|
+
start = i;
|
|
179
|
+
top = y;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
y += row_height;
|
|
183
|
+
i += 1;
|
|
184
|
+
}
|
|
185
|
+
while (i < items.length) {
|
|
186
|
+
y += height_map[i] || average_height;
|
|
187
|
+
i += 1;
|
|
188
|
+
if (y > scrollTop + viewport_height) break;
|
|
189
|
+
}
|
|
190
|
+
end = i;
|
|
191
|
+
const remaining = items.length - end;
|
|
192
|
+
average_height = y / end;
|
|
193
|
+
while (i < items.length) height_map[i++] = average_height;
|
|
194
|
+
bottom = remaining * average_height;
|
|
195
|
+
// prevent jumping if we scrolled up into unknown territory
|
|
196
|
+
if (start < old_start) {
|
|
197
|
+
await tick();
|
|
198
|
+
let expected_height = 0;
|
|
199
|
+
let actual_height = 0;
|
|
200
|
+
for (let i = start; i < old_start; i += 1) {
|
|
201
|
+
if (rows[i - start]) {
|
|
202
|
+
expected_height += height_map[i];
|
|
203
|
+
actual_height += item_height || rows[i - start].offsetHeight;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const d = actual_height - expected_height;
|
|
207
|
+
viewport.scrollTo(0, scrollTop + d);
|
|
208
|
+
}
|
|
209
|
+
// fire on:end event if we scrolled past the end of the list
|
|
210
|
+
if (end > items.length - end_threshold) {
|
|
211
|
+
if (items_count !== items.length) {
|
|
212
|
+
items_count = items.length;
|
|
213
|
+
await tick();
|
|
214
|
+
/**
|
|
215
|
+
* Fired when the list reaches `end_threshold` items before the end of the list.
|
|
216
|
+
*/
|
|
217
|
+
fire("end", { start, end });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const resize = () => {
|
|
223
|
+
viewport_height = viewport.offsetHeight;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// trigger initial refresh
|
|
227
|
+
onMount(() => {
|
|
228
|
+
rows = contents.getElementsByClassName("row");
|
|
229
|
+
mounted = true;
|
|
230
|
+
});
|
|
231
|
+
</script>
|
package/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export { default as TimelineItem } from "./helpers/Timeline/TimelineItem.svelte"
|
|
|
17
17
|
export { default as TimelineHeader } from "./helpers/Timeline/TimelineHeader.svelte";
|
|
18
18
|
export { default as Nl2br } from "./helpers/Nl2br.svelte";
|
|
19
19
|
export { default as ClipboardCopier } from "./helpers/ClipboardCopier.svelte";
|
|
20
|
+
export { default as ScrollableList } from "./helpers/ScrollableList.svelte";
|
|
20
21
|
export { alert, confirm, prompt, default as Dialog } from "./helpers/Dialog";
|
|
21
22
|
export {
|
|
22
23
|
Notifications,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kws3/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"description": "UI components for use with Svelte v3 applications.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
"text-mask-core": "^5.1.2",
|
|
30
30
|
"tippy.js": "^6.3.1"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "9fcbe3d34101490398b7bf571334e84e9ee04aee"
|
|
33
33
|
}
|
|
@@ -25,6 +25,8 @@ This will work only when `track_height` is set to `true`
|
|
|
25
25
|
: ''} {h_center ? 'h-centered' : ''} {active ? 'is-active' : ''} {klass}"
|
|
26
26
|
{style}>
|
|
27
27
|
<div
|
|
28
|
+
use:resizeObserver
|
|
29
|
+
on:resize={debouncedFireSizeChange}
|
|
28
30
|
bind:this={slideInner}
|
|
29
31
|
class="sliding-pane-inner {v_center ? 'v-centered' : ''} {h_center
|
|
30
32
|
? 'h-centered'
|
|
@@ -52,6 +54,10 @@ This will work only when `track_height` is set to `true`
|
|
|
52
54
|
<script>
|
|
53
55
|
import { onMount, createEventDispatcher } from "svelte";
|
|
54
56
|
import { debounce } from "@kws3/ui/utils";
|
|
57
|
+
import {
|
|
58
|
+
resizeObserver,
|
|
59
|
+
hasResizeObserver,
|
|
60
|
+
} from "@kws3/ui/utils/resizeObserver";
|
|
55
61
|
|
|
56
62
|
const fire = createEventDispatcher();
|
|
57
63
|
|
|
@@ -76,8 +82,7 @@ This will work only when `track_height` is set to `true`
|
|
|
76
82
|
*/
|
|
77
83
|
track_height = true;
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
let _height, slideInner, Observer;
|
|
85
|
+
let _height, slideInner;
|
|
81
86
|
|
|
82
87
|
/**
|
|
83
88
|
* CSS classes for the panel
|
|
@@ -91,18 +96,22 @@ This will work only when `track_height` is set to `true`
|
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
const max_retries_for_render = 10;
|
|
100
|
+
let try_count = 0;
|
|
94
101
|
function pollForRender() {
|
|
95
102
|
if (slideInner && typeof slideInner != "undefined") {
|
|
96
103
|
init();
|
|
97
104
|
} else {
|
|
98
105
|
setTimeout(() => {
|
|
99
|
-
|
|
106
|
+
try_count++;
|
|
107
|
+
if (try_count < max_retries_for_render) {
|
|
108
|
+
pollForRender();
|
|
109
|
+
}
|
|
100
110
|
}, 50);
|
|
101
111
|
}
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
function init() {
|
|
105
|
-
setupResizeObserver();
|
|
106
115
|
fireSizeChange();
|
|
107
116
|
}
|
|
108
117
|
|
|
@@ -129,23 +138,7 @@ This will work only when `track_height` is set to `true`
|
|
|
129
138
|
|
|
130
139
|
const debouncedFireSizeChange = debounce(fireSizeChange, 150, false);
|
|
131
140
|
|
|
132
|
-
const setupResizeObserver = () => {
|
|
133
|
-
if (hasResizeObserver) {
|
|
134
|
-
if (!slideInner || typeof slideInner == "undefined") {
|
|
135
|
-
pollForRender();
|
|
136
|
-
} else {
|
|
137
|
-
Observer = new ResizeObserver(() => {
|
|
138
|
-
debouncedFireSizeChange();
|
|
139
|
-
});
|
|
140
|
-
Observer.observe(slideInner);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
|
|
145
141
|
onMount(() => {
|
|
146
142
|
pollForRender();
|
|
147
|
-
return () => {
|
|
148
|
-
Observer && Observer.disconnect();
|
|
149
|
-
};
|
|
150
143
|
});
|
|
151
144
|
</script>
|
package/styles/ActionSheet.scss
CHANGED
|
@@ -2,6 +2,16 @@ $kws-actionsheet-background: $background !default;
|
|
|
2
2
|
$kws-actionsheet-box-shadow: $card-shadow !default;
|
|
3
3
|
$kws-actionsheet-box-radius: $radius !default;
|
|
4
4
|
|
|
5
|
+
.kws-action-sheet-outer {
|
|
6
|
+
display: flex;
|
|
7
|
+
visibility: hidden;
|
|
8
|
+
&.is-active {
|
|
9
|
+
visibility: visible;
|
|
10
|
+
}
|
|
11
|
+
.modal-background {
|
|
12
|
+
transition: all 0.3s;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
5
15
|
.kws-action-sheet {
|
|
6
16
|
border-radius: $kws-actionsheet-box-radius $kws-actionsheet-box-radius 0 0;
|
|
7
17
|
box-shadow: $kws-actionsheet-box-shadow;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const hasResizeObserver = typeof window.ResizeObserver != "undefined";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Usage: `<div use:resizeObserver on:resize={resizeHandler}>`
|
|
5
|
+
* @param {HTMLElement} node
|
|
6
|
+
* @returns {Object}
|
|
7
|
+
*/
|
|
8
|
+
export function resizeObserver(node) {
|
|
9
|
+
let ro;
|
|
10
|
+
if (hasResizeObserver) {
|
|
11
|
+
ro = new ResizeObserver(() => {
|
|
12
|
+
const e = new CustomEvent("resize", { bubbles: false });
|
|
13
|
+
node.dispatchEvent(e);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
ro.observe(node);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
destroy() {
|
|
21
|
+
hasResizeObserver && ro.disconnect();
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|