@kws3/ui 1.8.4 → 1.9.0
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 +5 -0
- package/buttons/SubmitButton.svelte +1 -1
- package/forms/AutoComplete.svelte +59 -17
- package/forms/select/MultiSelect.svelte +83 -30
- package/package.json +3 -2
package/CHANGELOG.mdx
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 1.9.0
|
|
2
|
+
- Use new fuzzy algorithm for `AutoComplete`,`MultiSelect` and `SearchableSelect` component
|
|
3
|
+
- Add property `scoreThreshold` in `AutoComplete`,`MultiSelect` and `SearchableSelect` component to control search accuracy.
|
|
4
|
+
- Bugfix: Keep `SubmitButton` disabled while it's not ready to submit yet
|
|
5
|
+
|
|
1
6
|
## 1.8.4
|
|
2
7
|
- New `AutoComplete` component
|
|
3
8
|
- Make options text size match the input `size` in `MultiSelect` and `SearchableSelect`.
|
|
@@ -15,6 +15,7 @@ Only send this prop if you want to fetch `options` asynchronously.
|
|
|
15
15
|
@param {'fuzzy'|'strict'} [search_strategy="fuzzy"] - Filtered options to be displayed strictly based on search text or perform a fuzzy match.
|
|
16
16
|
Fuzzy match will not work if `search` function is set, as the backend service is meant to do the matching., Default: `"fuzzy"`
|
|
17
17
|
@param {boolean} [highlighted_results=true] - Whether to show the highlighted or plain results in the dropdown., Default: `true`
|
|
18
|
+
@param {number} [scoreThreshold=5] - Score threshold for fuzzy search strategy, setting high score gives more fuzzy matches., Default: `5`
|
|
18
19
|
@param {''|'small'|'medium'|'large'} [size=""] - Size of the input, Default: `""`
|
|
19
20
|
@param {''|'primary'|'success'|'warning'|'info'|'danger'|'dark'|'light'} [color=""] - Color of the input, Default: `""`
|
|
20
21
|
@param {string} [style=""] - Inline CSS for input container, Default: `""`
|
|
@@ -97,7 +98,7 @@ Default value: `<span>{option.label}</span>`
|
|
|
97
98
|
import { debounce } from "@kws3/ui/utils";
|
|
98
99
|
import { createEventDispatcher, onMount, tick } from "svelte";
|
|
99
100
|
import { createPopper } from "@popperjs/core";
|
|
100
|
-
import
|
|
101
|
+
import fuzzy from "fuzzy.js";
|
|
101
102
|
|
|
102
103
|
const sameWidthPopperModifier = {
|
|
103
104
|
name: "sameWidth",
|
|
@@ -154,6 +155,11 @@ Default value: `<span>{option.label}</span>`
|
|
|
154
155
|
* Whether to show the highlighted or plain results in the dropdown.
|
|
155
156
|
*/
|
|
156
157
|
export let highlighted_results = true;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Score threshold for fuzzy search strategy, setting high score gives more fuzzy matches.
|
|
161
|
+
*/
|
|
162
|
+
export let scoreThreshold = 5;
|
|
157
163
|
/**
|
|
158
164
|
* Size of the input
|
|
159
165
|
* @type {''|'small'|'medium'|'large'}
|
|
@@ -260,12 +266,19 @@ Default value: `<span>{option.label}</span>`
|
|
|
260
266
|
// iterate over each word in the search query
|
|
261
267
|
let opts = [];
|
|
262
268
|
if (word) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
+
if (allow_fuzzy_match) {
|
|
270
|
+
opts = fuzzySearch(word, normalised_options);
|
|
271
|
+
} else {
|
|
272
|
+
opts = [...normalised_options].filter((item) => {
|
|
273
|
+
// filter out items that don't match `filter`
|
|
274
|
+
if (typeof item === "object" && item.value) {
|
|
275
|
+
return (
|
|
276
|
+
typeof item.value === "string" &&
|
|
277
|
+
item.value.toLowerCase().indexOf(word) > -1
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
269
282
|
}
|
|
270
283
|
|
|
271
284
|
cache[idx] = opts; // storing options to current index on cache
|
|
@@ -273,9 +286,9 @@ Default value: `<span>{option.label}</span>`
|
|
|
273
286
|
|
|
274
287
|
filtered_options = Object.values(cache) // get values from cache
|
|
275
288
|
.flat() // flatten array
|
|
276
|
-
.filter((v, i, self) => self.
|
|
289
|
+
.filter((v, i, self) => i === self.findIndex((t) => t.value === v.value)); // remove duplicates
|
|
277
290
|
|
|
278
|
-
if (highlighted_results) {
|
|
291
|
+
if (highlighted_results && !allow_fuzzy_match) {
|
|
279
292
|
filtered_options = highlightMatches(filtered_options, filters);
|
|
280
293
|
}
|
|
281
294
|
setOptionsVisible(true);
|
|
@@ -312,6 +325,17 @@ Default value: `<span>{option.label}</span>`
|
|
|
312
325
|
modifiers: [sameWidthPopperModifier],
|
|
313
326
|
});
|
|
314
327
|
|
|
328
|
+
if (allow_fuzzy_match && fuzzy) {
|
|
329
|
+
fuzzy.analyzeSubTerms = true;
|
|
330
|
+
fuzzy.analyzeSubTermDepth = 10;
|
|
331
|
+
fuzzy.highlighting.before = "";
|
|
332
|
+
fuzzy.highlighting.after = "";
|
|
333
|
+
if (highlighted_results) {
|
|
334
|
+
fuzzy.highlighting.before = `<span class="h">`;
|
|
335
|
+
fuzzy.highlighting.after = "</span>";
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
315
339
|
//normalize value
|
|
316
340
|
if (value === null || typeof value == "undefined") {
|
|
317
341
|
value = null;
|
|
@@ -386,13 +410,6 @@ Default value: `<span>{option.label}</span>`
|
|
|
386
410
|
fire("blur");
|
|
387
411
|
}
|
|
388
412
|
|
|
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
413
|
const normaliseArraysToObjects = (arr) =>
|
|
397
414
|
[...arr].map((item) =>
|
|
398
415
|
typeof item === "object" ? item : { label: item, value: item }
|
|
@@ -404,7 +421,10 @@ Default value: `<span>{option.label}</span>`
|
|
|
404
421
|
let common_chars = [...filters.join("")].filter(
|
|
405
422
|
(v, i, self) => self.indexOf(v) === i
|
|
406
423
|
);
|
|
407
|
-
let pattern = new RegExp(
|
|
424
|
+
let pattern = new RegExp(
|
|
425
|
+
`[${common_chars.join("").replace(/\\/g, "\")}]`,
|
|
426
|
+
"gi"
|
|
427
|
+
);
|
|
408
428
|
return options.map((item) => {
|
|
409
429
|
return {
|
|
410
430
|
...item,
|
|
@@ -423,4 +443,26 @@ Default value: `<span>{option.label}</span>`
|
|
|
423
443
|
function sanitizeFilters(v) {
|
|
424
444
|
return v && v.trim() ? v.toLowerCase().trim().split(/\s+/) : [];
|
|
425
445
|
}
|
|
446
|
+
|
|
447
|
+
function fuzzySearch(word, options) {
|
|
448
|
+
let OPTS = options.map((item) => {
|
|
449
|
+
let output = fuzzy(item.value, word);
|
|
450
|
+
item = { ...output, ...item };
|
|
451
|
+
item.label = output.highlightedTerm;
|
|
452
|
+
item.score =
|
|
453
|
+
!item.score || (item.score && item.score < output.score)
|
|
454
|
+
? output.score
|
|
455
|
+
: item.score || 0;
|
|
456
|
+
return item;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
let maxScore = Math.max(...OPTS.map((i) => i.score));
|
|
460
|
+
let calculatedLimit = maxScore - scoreThreshold;
|
|
461
|
+
|
|
462
|
+
OPTS = OPTS.filter(
|
|
463
|
+
(r) => r.score > (calculatedLimit > 0 ? calculatedLimit : 0)
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
return OPTS;
|
|
467
|
+
}
|
|
426
468
|
</script>
|
|
@@ -22,6 +22,7 @@ Only send this prop if you want to fetch `options` asynchronously.
|
|
|
22
22
|
`options` prop will be ignored if this prop is set., Default: `null`
|
|
23
23
|
@param {'fuzzy'|'strict'} [search_strategy="fuzzy"] - Filtered options to be displayed strictly based on search text or perform a fuzzy match.
|
|
24
24
|
Fuzzy match will not work if `search` function is set, as the backend service is meant to do the matching., Default: `"fuzzy"`
|
|
25
|
+
@param {number} [scoreThreshold=3] - Score threshold for fuzzy search strategy, setting high score gives more fuzzy matches., Default: `3`
|
|
25
26
|
@param {''|'small'|'medium'|'large'} [size=""] - Size of the input, Default: `""`
|
|
26
27
|
@param {''|'primary'|'success'|'warning'|'info'|'danger'|'dark'|'light'} [color=""] - Color of the input, Default: `""`
|
|
27
28
|
@param {string} [style=""] - Inline CSS for input container, Default: `""`
|
|
@@ -161,7 +162,7 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
161
162
|
import { debounce } from "@kws3/ui/utils";
|
|
162
163
|
import { createEventDispatcher, onMount, tick } from "svelte";
|
|
163
164
|
import { createPopper } from "@popperjs/core";
|
|
164
|
-
import
|
|
165
|
+
import fuzzy from "fuzzy.js";
|
|
165
166
|
|
|
166
167
|
const sameWidthPopperModifier = {
|
|
167
168
|
name: "sameWidth",
|
|
@@ -230,6 +231,11 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
230
231
|
* @type {'fuzzy'|'strict'}
|
|
231
232
|
*/
|
|
232
233
|
export let search_strategy = "fuzzy";
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Score threshold for fuzzy search strategy, setting high score gives more fuzzy matches.
|
|
237
|
+
*/
|
|
238
|
+
export let scoreThreshold = 3;
|
|
233
239
|
/**
|
|
234
240
|
* Size of the input
|
|
235
241
|
* @type {''|'small'|'medium'|'large'}
|
|
@@ -357,11 +363,7 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
357
363
|
|
|
358
364
|
$: value, single, fillSelectedOptions();
|
|
359
365
|
|
|
360
|
-
$:
|
|
361
|
-
(activeOption && !filteredOptions.includes(activeOption)) ||
|
|
362
|
-
(!activeOption && searchText)
|
|
363
|
-
)
|
|
364
|
-
activeOption = filteredOptions[0];
|
|
366
|
+
$: activeOption, searchText, filteredOptions, updateActiveOption();
|
|
365
367
|
|
|
366
368
|
//TODO: optimise isSelected function
|
|
367
369
|
$: isSelected = (option) => {
|
|
@@ -402,23 +404,26 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
402
404
|
if (asyncMode && searching) {
|
|
403
405
|
debouncedTriggerSearch(filter);
|
|
404
406
|
} else {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
407
|
+
if (allow_fuzzy_match) {
|
|
408
|
+
filteredOptions = fuzzySearch(filter, [...normalisedOptions]);
|
|
409
|
+
} else {
|
|
410
|
+
filteredOptions = strictSearch(filter, [...normalisedOptions]);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function updateActiveOption() {
|
|
416
|
+
if (
|
|
417
|
+
(activeOption && searching && !filteredOptions.includes(activeOption)) ||
|
|
418
|
+
(!activeOption && searchText)
|
|
419
|
+
) {
|
|
420
|
+
activeOption = filteredOptions[0];
|
|
421
|
+
} else {
|
|
422
|
+
if (allow_fuzzy_match) {
|
|
423
|
+
activeOption = filteredOptions.find((opts) =>
|
|
424
|
+
matchesValue(activeOption, opts)
|
|
425
|
+
);
|
|
426
|
+
}
|
|
422
427
|
}
|
|
423
428
|
}
|
|
424
429
|
|
|
@@ -474,6 +479,11 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
474
479
|
modifiers: [sameWidthPopperModifier],
|
|
475
480
|
});
|
|
476
481
|
|
|
482
|
+
if (allow_fuzzy_match && fuzzy) {
|
|
483
|
+
fuzzy.analyzeSubTerms = true;
|
|
484
|
+
fuzzy.analyzeSubTermDepth = 10;
|
|
485
|
+
}
|
|
486
|
+
|
|
477
487
|
//normalize value for single versus multiselect
|
|
478
488
|
if (value === null || typeof value == "undefined") {
|
|
479
489
|
value = single ? null : [];
|
|
@@ -674,16 +684,15 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
674
684
|
if (_value === null || typeof _value == "undefined") {
|
|
675
685
|
return false;
|
|
676
686
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
687
|
+
let value =
|
|
688
|
+
typeof _value === "object" && `${used_value_key}` in _value
|
|
689
|
+
? _value[used_value_key]
|
|
690
|
+
: _value;
|
|
691
|
+
return `${value}` === `${_option[used_value_key]}`;
|
|
680
692
|
};
|
|
681
693
|
|
|
682
694
|
const match = (needle, haystack) => {
|
|
683
|
-
|
|
684
|
-
return allow_fuzzy_match
|
|
685
|
-
? fuzzysearch(needle, _hayStack)
|
|
686
|
-
: _hayStack.indexOf(needle) > -1;
|
|
695
|
+
return haystack.toLowerCase().indexOf(needle) > -1;
|
|
687
696
|
};
|
|
688
697
|
|
|
689
698
|
const normaliseArraysToObjects = (arr) => {
|
|
@@ -704,4 +713,48 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
704
713
|
searching = false;
|
|
705
714
|
});
|
|
706
715
|
};
|
|
716
|
+
|
|
717
|
+
function fuzzySearch(filter, options) {
|
|
718
|
+
if (!filter) return options;
|
|
719
|
+
if (options.length) {
|
|
720
|
+
let OPTS = options.map((item) => {
|
|
721
|
+
let output = fuzzy(item[used_search_key], filter);
|
|
722
|
+
item = { ...output, original: item };
|
|
723
|
+
item.score =
|
|
724
|
+
!item.score || (item.score && item.score < output.score)
|
|
725
|
+
? output.score
|
|
726
|
+
: item.score || 0;
|
|
727
|
+
return item;
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
let maxScore = Math.max(...OPTS.map((i) => i.score));
|
|
731
|
+
let calculatedLimit = maxScore - scoreThreshold;
|
|
732
|
+
|
|
733
|
+
OPTS = OPTS.filter(
|
|
734
|
+
(r) => r.score > (calculatedLimit > 0 ? calculatedLimit : 0)
|
|
735
|
+
).map((o) => o.original);
|
|
736
|
+
|
|
737
|
+
return OPTS;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function strictSearch(filter, options) {
|
|
742
|
+
return options.filter((item) => {
|
|
743
|
+
// filter out items that don't match `filter`
|
|
744
|
+
if (typeof item === "object") {
|
|
745
|
+
if (used_search_key) {
|
|
746
|
+
return (
|
|
747
|
+
typeof item[used_search_key] === "string" &&
|
|
748
|
+
match(filter, item[used_search_key])
|
|
749
|
+
);
|
|
750
|
+
} else {
|
|
751
|
+
for (var key in item) {
|
|
752
|
+
return typeof item[key] === "string" && match(filter, item[key]);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
return match(filter, item);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
707
760
|
</script>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kws3/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "UI components for use with Svelte v3 applications.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"apexcharts": "3.33.2",
|
|
27
27
|
"flatpickr": "^4.5.2",
|
|
28
|
+
"fuzzy.js": "^0.1.0",
|
|
28
29
|
"svelte-portal": "^2.1.2",
|
|
29
30
|
"text-mask-core": "^5.1.2",
|
|
30
31
|
"tippy.js": "^6.3.1"
|
|
31
32
|
},
|
|
32
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "8862e5927943ef0610fe5f2f37630d977aff411f"
|
|
33
34
|
}
|