@kws3/ui 1.9.1 → 1.9.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 +5 -0
- package/forms/AutoComplete.svelte +53 -48
- package/forms/select/MultiSelect.svelte +20 -22
- package/package.json +2 -3
- package/utils/fuzzy.js +117 -0
- package/utils/fuzzysearch.js +39 -17
package/CHANGELOG.mdx
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 1.9.2
|
|
2
|
+
- Bug fix with fuzzy.js and Vite prebundling
|
|
3
|
+
- Debounce fuzzy searches
|
|
4
|
+
- Expose `@kws3/ui/utils/fuzzysearch` as a reusable function
|
|
5
|
+
|
|
1
6
|
## 1.9.1
|
|
2
7
|
- `SearchableSelect` and `MultiSelect`: match colors of dropdown area to theme color when dropdown area is inside a `Portal`
|
|
3
8
|
- `AutoComplete`: match colors of dropdown area to theme color when dropdown area is inside a `Portal`
|
|
@@ -100,7 +100,7 @@ Default value: `<span>{option.label}</span>`
|
|
|
100
100
|
import { debounce } from "@kws3/ui/utils";
|
|
101
101
|
import { createEventDispatcher, onMount, tick } from "svelte";
|
|
102
102
|
import { createPopper } from "@popperjs/core";
|
|
103
|
-
import fuzzy from "
|
|
103
|
+
import { fuzzy, fuzzysearch } from "../utils/fuzzysearch";
|
|
104
104
|
|
|
105
105
|
const sameWidthPopperModifier = {
|
|
106
106
|
name: "sameWidth",
|
|
@@ -262,38 +262,11 @@ Default value: `<span>{option.label}</span>`
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
function triggerSearch(filters) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
let opts = [];
|
|
270
|
-
if (word) {
|
|
271
|
-
if (allow_fuzzy_match) {
|
|
272
|
-
opts = fuzzySearch(word, normalised_options);
|
|
273
|
-
} else {
|
|
274
|
-
opts = [...normalised_options].filter((item) => {
|
|
275
|
-
// filter out items that don't match `filter`
|
|
276
|
-
if (typeof item === "object" && item.value) {
|
|
277
|
-
return (
|
|
278
|
-
typeof item.value === "string" &&
|
|
279
|
-
item.value.toLowerCase().indexOf(word) > -1
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
cache[idx] = opts; // storing options to current index on cache
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
filtered_options = Object.values(cache) // get values from cache
|
|
290
|
-
.flat() // flatten array
|
|
291
|
-
.filter((v, i, self) => i === self.findIndex((t) => t.value === v.value)); // remove duplicates
|
|
292
|
-
|
|
293
|
-
if (highlighted_results && !allow_fuzzy_match) {
|
|
294
|
-
filtered_options = highlightMatches(filtered_options, filters);
|
|
265
|
+
if (allow_fuzzy_match) {
|
|
266
|
+
debouncedFuzzySearch(filters, [...normalised_options]);
|
|
267
|
+
} else {
|
|
268
|
+
searchInStrictMode(filters, [...normalised_options]);
|
|
295
269
|
}
|
|
296
|
-
setOptionsVisible(true);
|
|
297
270
|
}
|
|
298
271
|
|
|
299
272
|
function triggerExternalSearch(filters) {
|
|
@@ -446,25 +419,57 @@ Default value: `<span>{option.label}</span>`
|
|
|
446
419
|
return v && v.trim() ? v.toLowerCase().trim().split(/\s+/) : [];
|
|
447
420
|
}
|
|
448
421
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
422
|
+
const debouncedFuzzySearch = debounce(searchInFuzzyMode, 200, false);
|
|
423
|
+
|
|
424
|
+
function searchInFuzzyMode(filters, options) {
|
|
425
|
+
let cache = {};
|
|
426
|
+
//TODO - can optimize more for very long lists
|
|
427
|
+
filters.forEach((word, idx) => {
|
|
428
|
+
// iterate over each word in the search query
|
|
429
|
+
let opts = [];
|
|
430
|
+
if (word) {
|
|
431
|
+
let result = fuzzysearch(word, options, {
|
|
432
|
+
search_key: "label",
|
|
433
|
+
scoreThreshold,
|
|
434
|
+
});
|
|
435
|
+
opts = result;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
cache[idx] = opts; // storing options to current index on cache
|
|
459
439
|
});
|
|
440
|
+
setFilteredOptions(cache);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function searchInStrictMode(filters, options) {
|
|
444
|
+
let cache = {};
|
|
445
|
+
filters.forEach((word, idx) => {
|
|
446
|
+
// iterate over each word in the search query
|
|
447
|
+
let opts = [];
|
|
448
|
+
if (word) {
|
|
449
|
+
opts = options.filter((item) => {
|
|
450
|
+
// filter out items that don't match `filter`
|
|
451
|
+
if (typeof item === "object" && item.value) {
|
|
452
|
+
return (
|
|
453
|
+
typeof item.value === "string" &&
|
|
454
|
+
item.value.toLowerCase().indexOf(word) > -1
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
460
459
|
|
|
461
|
-
|
|
462
|
-
|
|
460
|
+
cache[idx] = opts; // storing options to current index on cache
|
|
461
|
+
});
|
|
462
|
+
setFilteredOptions(cache, filters);
|
|
463
|
+
}
|
|
463
464
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
465
|
+
function setFilteredOptions(cache, filters) {
|
|
466
|
+
filtered_options = Object.values(cache) // get values from cache
|
|
467
|
+
.flat() // flatten array
|
|
468
|
+
.filter((v, i, self) => i === self.findIndex((t) => t.value === v.value)); // remove duplicates
|
|
467
469
|
|
|
468
|
-
|
|
470
|
+
if (highlighted_results && !allow_fuzzy_match) {
|
|
471
|
+
filtered_options = highlightMatches(filtered_options, filters);
|
|
472
|
+
}
|
|
473
|
+
setOptionsVisible(true);
|
|
469
474
|
}
|
|
470
475
|
</script>
|
|
@@ -164,7 +164,7 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
164
164
|
import { debounce } from "@kws3/ui/utils";
|
|
165
165
|
import { createEventDispatcher, onMount, tick } from "svelte";
|
|
166
166
|
import { createPopper } from "@popperjs/core";
|
|
167
|
-
import fuzzy from "
|
|
167
|
+
import { fuzzy, fuzzysearch } from "../../utils/fuzzysearch";
|
|
168
168
|
|
|
169
169
|
const sameWidthPopperModifier = {
|
|
170
170
|
name: "sameWidth",
|
|
@@ -407,7 +407,7 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
407
407
|
debouncedTriggerSearch(filter);
|
|
408
408
|
} else {
|
|
409
409
|
if (allow_fuzzy_match) {
|
|
410
|
-
|
|
410
|
+
fuzzySearch(filter, [...normalisedOptions]);
|
|
411
411
|
} else {
|
|
412
412
|
filteredOptions = strictSearch(filter, [...normalisedOptions]);
|
|
413
413
|
}
|
|
@@ -416,7 +416,11 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
416
416
|
|
|
417
417
|
function updateActiveOption() {
|
|
418
418
|
if (
|
|
419
|
-
(activeOption &&
|
|
419
|
+
(activeOption &&
|
|
420
|
+
searching &&
|
|
421
|
+
!filteredOptions.some(
|
|
422
|
+
(fo) => fo[used_value_key] === activeOption[used_value_key]
|
|
423
|
+
)) ||
|
|
420
424
|
(!activeOption && searchText)
|
|
421
425
|
) {
|
|
422
426
|
activeOption = filteredOptions[0];
|
|
@@ -484,6 +488,8 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
484
488
|
if (allow_fuzzy_match && fuzzy) {
|
|
485
489
|
fuzzy.analyzeSubTerms = true;
|
|
486
490
|
fuzzy.analyzeSubTermDepth = 10;
|
|
491
|
+
fuzzy.highlighting.before = "";
|
|
492
|
+
fuzzy.highlighting.after = "";
|
|
487
493
|
}
|
|
488
494
|
|
|
489
495
|
//normalize value for single versus multiselect
|
|
@@ -716,27 +722,19 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
716
722
|
});
|
|
717
723
|
};
|
|
718
724
|
|
|
719
|
-
|
|
720
|
-
|
|
725
|
+
const fuzzySearch = debounce(searchInFuzzyMode, 200, false);
|
|
726
|
+
|
|
727
|
+
function searchInFuzzyMode(filter, options) {
|
|
728
|
+
if (!filter) {
|
|
729
|
+
filteredOptions = options;
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
721
732
|
if (options.length) {
|
|
722
|
-
let
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
item.score =
|
|
726
|
-
!item.score || (item.score && item.score < output.score)
|
|
727
|
-
? output.score
|
|
728
|
-
: item.score || 0;
|
|
729
|
-
return item;
|
|
733
|
+
let result = fuzzysearch(filter, options, {
|
|
734
|
+
search_key: used_search_key,
|
|
735
|
+
scoreThreshold,
|
|
730
736
|
});
|
|
731
|
-
|
|
732
|
-
let maxScore = Math.max(...OPTS.map((i) => i.score));
|
|
733
|
-
let calculatedLimit = maxScore - scoreThreshold;
|
|
734
|
-
|
|
735
|
-
OPTS = OPTS.filter(
|
|
736
|
-
(r) => r.score > (calculatedLimit > 0 ? calculatedLimit : 0)
|
|
737
|
-
).map((o) => o.original);
|
|
738
|
-
|
|
739
|
-
return OPTS;
|
|
737
|
+
filteredOptions = result;
|
|
740
738
|
}
|
|
741
739
|
}
|
|
742
740
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kws3/ui",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2",
|
|
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
|
-
"fuzzy.js": "^0.1.0",
|
|
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": "
|
|
32
|
+
"gitHead": "9e6e5f8d4dbb1e5f0a40e3dade66bdfd5f31fd4d"
|
|
34
33
|
}
|
package/utils/fuzzy.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fuzzy.js v0.1.0
|
|
3
|
+
* (c) 2016 Ben Ripkens
|
|
4
|
+
* @license: MIT
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* Adapted from fuzzy.js for @kws3/ui to work with vite prebundling
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
var fuzzy = function fuzzy(term, query) {
|
|
12
|
+
var max = calcFuzzyScore(term, query);
|
|
13
|
+
var termLength = term.length;
|
|
14
|
+
|
|
15
|
+
if (fuzzy.analyzeSubTerms) {
|
|
16
|
+
for (var i = 1; i < termLength && i < fuzzy.analyzeSubTermDepth; i++) {
|
|
17
|
+
var subTerm = term.substring(i);
|
|
18
|
+
var score = calcFuzzyScore(subTerm, query);
|
|
19
|
+
if (score.score > max.score) {
|
|
20
|
+
// we need to correct 'term' and 'matchedTerm', as calcFuzzyScore
|
|
21
|
+
// does not now that it operates on a substring. Doing it only for
|
|
22
|
+
// new maximum score to save some performance.
|
|
23
|
+
score.term = term;
|
|
24
|
+
score.highlightedTerm = term.substring(0, i) + score.highlightedTerm;
|
|
25
|
+
max = score;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return max;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
var calcFuzzyScore = function calcFuzzyScore(term, query) {
|
|
34
|
+
var score = 0;
|
|
35
|
+
var termLength = term.length;
|
|
36
|
+
var queryLength = query.length;
|
|
37
|
+
var highlighting = "";
|
|
38
|
+
var ti = 0;
|
|
39
|
+
// -1 would not work as this would break the calculations of bonus
|
|
40
|
+
// points for subsequent character matches. Something like
|
|
41
|
+
// Number.MIN_VALUE would be more appropriate, but unfortunately
|
|
42
|
+
// Number.MIN_VALUE + 1 equals 1...
|
|
43
|
+
var previousMatchingCharacter = -2;
|
|
44
|
+
|
|
45
|
+
for (var qi = 0; qi < queryLength && ti < termLength; qi++) {
|
|
46
|
+
var qc = query.charAt(qi);
|
|
47
|
+
var lowerQc = qc.toLowerCase();
|
|
48
|
+
|
|
49
|
+
for (; ti < termLength; ti++) {
|
|
50
|
+
var tc = term.charAt(ti);
|
|
51
|
+
|
|
52
|
+
if (lowerQc === tc.toLowerCase()) {
|
|
53
|
+
score++;
|
|
54
|
+
|
|
55
|
+
if (previousMatchingCharacter + 1 === ti) {
|
|
56
|
+
score += 2;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
highlighting +=
|
|
60
|
+
fuzzy.highlighting.before + tc + fuzzy.highlighting.after;
|
|
61
|
+
previousMatchingCharacter = ti;
|
|
62
|
+
ti++;
|
|
63
|
+
break;
|
|
64
|
+
} else {
|
|
65
|
+
highlighting += tc;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
highlighting += term.substring(ti, term.length);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
score: score,
|
|
74
|
+
term: term,
|
|
75
|
+
query: query,
|
|
76
|
+
highlightedTerm: highlighting,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
fuzzy.matchComparator = function matchComparator(m1, m2) {
|
|
81
|
+
return m2.score - m1.score !== 0
|
|
82
|
+
? m2.score - m1.score
|
|
83
|
+
: m1.term.length - m2.term.length;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/*
|
|
87
|
+
* Whether or not fuzzy.js should analyze sub-terms, i.e. also
|
|
88
|
+
* check term starting positions != 0.
|
|
89
|
+
*
|
|
90
|
+
* Example:
|
|
91
|
+
* Given the term 'Halleluja' and query 'luja'
|
|
92
|
+
*
|
|
93
|
+
* Fuzzy.js scores this combination with an 8, when analyzeSubTerms is
|
|
94
|
+
* set to false, as the following matching string will be calculated:
|
|
95
|
+
* Ha[l]lel[uja]
|
|
96
|
+
*
|
|
97
|
+
* If you activate sub temr analysis though, the query will reach a score
|
|
98
|
+
* of 10, as the matching string looks as following:
|
|
99
|
+
* Halle[luja]
|
|
100
|
+
*
|
|
101
|
+
* Naturally, the second version is more expensive than the first one.
|
|
102
|
+
* You should therefore configure how many sub terms you which to analyse.
|
|
103
|
+
* This can be configured through fuzzy.analyzeSubTermDepth = 10.
|
|
104
|
+
*/
|
|
105
|
+
fuzzy.analyzeSubTerms = false;
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
* How many sub terms should be analyzed.
|
|
109
|
+
*/
|
|
110
|
+
fuzzy.analyzeSubTermDepth = 10;
|
|
111
|
+
|
|
112
|
+
fuzzy.highlighting = {
|
|
113
|
+
before: "<em>",
|
|
114
|
+
after: "</em>",
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export default fuzzy;
|
package/utils/fuzzysearch.js
CHANGED
|
@@ -1,20 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (haystack.charCodeAt(j++) === nch) {
|
|
14
|
-
continue outer;
|
|
1
|
+
import fuzzy from "./fuzzy.js";
|
|
2
|
+
|
|
3
|
+
export function fuzzysearch(needle, haystack, opts) {
|
|
4
|
+
let search_key = defaultValue(opts, "search_key", "value");
|
|
5
|
+
let scoreThreshold = defaultValue(opts, "scoreThreshold", 5);
|
|
6
|
+
|
|
7
|
+
let OPTS = haystack.map((option) => {
|
|
8
|
+
let item = { ...option };
|
|
9
|
+
item.original = { ...option };
|
|
10
|
+
if (typeof item === "object") {
|
|
11
|
+
if (!Array.isArray(search_key)) {
|
|
12
|
+
search_key = [search_key];
|
|
15
13
|
}
|
|
14
|
+
|
|
15
|
+
search_key.forEach((s_key) => {
|
|
16
|
+
if (`${s_key}` in item) {
|
|
17
|
+
let output = fuzzy(option[s_key], needle);
|
|
18
|
+
item.original[s_key] = output.highlightedTerm;
|
|
19
|
+
item.score =
|
|
20
|
+
!item.score || (item.score && item.score < output.score)
|
|
21
|
+
? output.score
|
|
22
|
+
: item.score || 0;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
16
25
|
}
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
|
|
26
|
+
return item;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let maxScore = Math.max(...OPTS.map((i) => i.score));
|
|
30
|
+
let calculatedLimit = maxScore - scoreThreshold;
|
|
31
|
+
|
|
32
|
+
OPTS = OPTS.filter(
|
|
33
|
+
(r) => r.score > (calculatedLimit > 0 ? calculatedLimit : 0)
|
|
34
|
+
);
|
|
35
|
+
return OPTS.map((i) => i.original);
|
|
20
36
|
}
|
|
37
|
+
|
|
38
|
+
function defaultValue(opts, key, value) {
|
|
39
|
+
return opts && opts[key] ? opts[key] : value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { fuzzy };
|