@kws3/ui 1.8.4 → 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 +14 -0
- package/buttons/SubmitButton.svelte +1 -1
- package/forms/AutoComplete.svelte +83 -34
- package/forms/select/MultiSelect.svelte +84 -31
- package/package.json +2 -2
- package/styles/Select.scss +1 -1
- package/utils/fuzzy.js +117 -0
- package/utils/fuzzysearch.js +39 -17
package/CHANGELOG.mdx
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
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
|
+
|
|
6
|
+
## 1.9.1
|
|
7
|
+
- `SearchableSelect` and `MultiSelect`: match colors of dropdown area to theme color when dropdown area is inside a `Portal`
|
|
8
|
+
- `AutoComplete`: match colors of dropdown area to theme color when dropdown area is inside a `Portal`
|
|
9
|
+
|
|
10
|
+
## 1.9.0
|
|
11
|
+
- Use new fuzzy algorithm for `AutoComplete`,`MultiSelect` and `SearchableSelect` component
|
|
12
|
+
- Add property `scoreThreshold` in `AutoComplete`,`MultiSelect` and `SearchableSelect` component to control search accuracy.
|
|
13
|
+
- Bugfix: Keep `SubmitButton` disabled while it's not ready to submit yet
|
|
14
|
+
|
|
1
15
|
## 1.8.4
|
|
2
16
|
- New `AutoComplete` component
|
|
3
17
|
- 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: `""`
|
|
@@ -65,7 +66,9 @@ Default value: `<span>{option.label}</span>`
|
|
|
65
66
|
class="button is-paddingless delete is-medium is-loading" />
|
|
66
67
|
{/if}
|
|
67
68
|
{#if rootContainer}
|
|
68
|
-
<div
|
|
69
|
+
<div
|
|
70
|
+
class="kws-autocomplete is-{color || 'primary'}"
|
|
71
|
+
use:portal={dropdown_portal}>
|
|
69
72
|
<ul bind:this={dropdown} class="options" class:hidden={!show_options}>
|
|
70
73
|
{#each filtered_options as option}
|
|
71
74
|
<li
|
|
@@ -97,7 +100,7 @@ Default value: `<span>{option.label}</span>`
|
|
|
97
100
|
import { debounce } from "@kws3/ui/utils";
|
|
98
101
|
import { createEventDispatcher, onMount, tick } from "svelte";
|
|
99
102
|
import { createPopper } from "@popperjs/core";
|
|
100
|
-
import fuzzysearch from "
|
|
103
|
+
import { fuzzy, fuzzysearch } from "../utils/fuzzysearch";
|
|
101
104
|
|
|
102
105
|
const sameWidthPopperModifier = {
|
|
103
106
|
name: "sameWidth",
|
|
@@ -154,6 +157,11 @@ Default value: `<span>{option.label}</span>`
|
|
|
154
157
|
* Whether to show the highlighted or plain results in the dropdown.
|
|
155
158
|
*/
|
|
156
159
|
export let highlighted_results = true;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Score threshold for fuzzy search strategy, setting high score gives more fuzzy matches.
|
|
163
|
+
*/
|
|
164
|
+
export let scoreThreshold = 5;
|
|
157
165
|
/**
|
|
158
166
|
* Size of the input
|
|
159
167
|
* @type {''|'small'|'medium'|'large'}
|
|
@@ -254,31 +262,11 @@ Default value: `<span>{option.label}</span>`
|
|
|
254
262
|
}
|
|
255
263
|
|
|
256
264
|
function triggerSearch(filters) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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);
|
|
265
|
+
if (allow_fuzzy_match) {
|
|
266
|
+
debouncedFuzzySearch(filters, [...normalised_options]);
|
|
267
|
+
} else {
|
|
268
|
+
searchInStrictMode(filters, [...normalised_options]);
|
|
280
269
|
}
|
|
281
|
-
setOptionsVisible(true);
|
|
282
270
|
}
|
|
283
271
|
|
|
284
272
|
function triggerExternalSearch(filters) {
|
|
@@ -312,6 +300,17 @@ Default value: `<span>{option.label}</span>`
|
|
|
312
300
|
modifiers: [sameWidthPopperModifier],
|
|
313
301
|
});
|
|
314
302
|
|
|
303
|
+
if (allow_fuzzy_match && fuzzy) {
|
|
304
|
+
fuzzy.analyzeSubTerms = true;
|
|
305
|
+
fuzzy.analyzeSubTermDepth = 10;
|
|
306
|
+
fuzzy.highlighting.before = "";
|
|
307
|
+
fuzzy.highlighting.after = "";
|
|
308
|
+
if (highlighted_results) {
|
|
309
|
+
fuzzy.highlighting.before = `<span class="h">`;
|
|
310
|
+
fuzzy.highlighting.after = "</span>";
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
315
314
|
//normalize value
|
|
316
315
|
if (value === null || typeof value == "undefined") {
|
|
317
316
|
value = null;
|
|
@@ -386,13 +385,6 @@ Default value: `<span>{option.label}</span>`
|
|
|
386
385
|
fire("blur");
|
|
387
386
|
}
|
|
388
387
|
|
|
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
388
|
const normaliseArraysToObjects = (arr) =>
|
|
397
389
|
[...arr].map((item) =>
|
|
398
390
|
typeof item === "object" ? item : { label: item, value: item }
|
|
@@ -404,7 +396,10 @@ Default value: `<span>{option.label}</span>`
|
|
|
404
396
|
let common_chars = [...filters.join("")].filter(
|
|
405
397
|
(v, i, self) => self.indexOf(v) === i
|
|
406
398
|
);
|
|
407
|
-
let pattern = new RegExp(
|
|
399
|
+
let pattern = new RegExp(
|
|
400
|
+
`[${common_chars.join("").replace(/\\/g, "\")}]`,
|
|
401
|
+
"gi"
|
|
402
|
+
);
|
|
408
403
|
return options.map((item) => {
|
|
409
404
|
return {
|
|
410
405
|
...item,
|
|
@@ -423,4 +418,58 @@ Default value: `<span>{option.label}</span>`
|
|
|
423
418
|
function sanitizeFilters(v) {
|
|
424
419
|
return v && v.trim() ? v.toLowerCase().trim().split(/\s+/) : [];
|
|
425
420
|
}
|
|
421
|
+
|
|
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
|
|
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
|
+
}
|
|
459
|
+
|
|
460
|
+
cache[idx] = opts; // storing options to current index on cache
|
|
461
|
+
});
|
|
462
|
+
setFilteredOptions(cache, filters);
|
|
463
|
+
}
|
|
464
|
+
|
|
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
|
|
469
|
+
|
|
470
|
+
if (highlighted_results && !allow_fuzzy_match) {
|
|
471
|
+
filtered_options = highlightMatches(filtered_options, filters);
|
|
472
|
+
}
|
|
473
|
+
setOptionsVisible(true);
|
|
474
|
+
}
|
|
426
475
|
</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: `""`
|
|
@@ -119,7 +120,9 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
119
120
|
style={shouldShowClearAll ? "" : "display: none;"} />
|
|
120
121
|
{/if}
|
|
121
122
|
{#if rootContainer}
|
|
122
|
-
<div
|
|
123
|
+
<div
|
|
124
|
+
class="kws-searchableselect is-{color || 'primary'}"
|
|
125
|
+
use:portal={dropdown_portal}>
|
|
123
126
|
<ul
|
|
124
127
|
bind:this={dropdown}
|
|
125
128
|
class="options {single ? 'is-single' : 'is-multi'}"
|
|
@@ -161,7 +164,7 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
161
164
|
import { debounce } from "@kws3/ui/utils";
|
|
162
165
|
import { createEventDispatcher, onMount, tick } from "svelte";
|
|
163
166
|
import { createPopper } from "@popperjs/core";
|
|
164
|
-
import fuzzysearch from "
|
|
167
|
+
import { fuzzy, fuzzysearch } from "../../utils/fuzzysearch";
|
|
165
168
|
|
|
166
169
|
const sameWidthPopperModifier = {
|
|
167
170
|
name: "sameWidth",
|
|
@@ -230,6 +233,11 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
230
233
|
* @type {'fuzzy'|'strict'}
|
|
231
234
|
*/
|
|
232
235
|
export let search_strategy = "fuzzy";
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Score threshold for fuzzy search strategy, setting high score gives more fuzzy matches.
|
|
239
|
+
*/
|
|
240
|
+
export let scoreThreshold = 3;
|
|
233
241
|
/**
|
|
234
242
|
* Size of the input
|
|
235
243
|
* @type {''|'small'|'medium'|'large'}
|
|
@@ -357,11 +365,7 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
357
365
|
|
|
358
366
|
$: value, single, fillSelectedOptions();
|
|
359
367
|
|
|
360
|
-
$:
|
|
361
|
-
(activeOption && !filteredOptions.includes(activeOption)) ||
|
|
362
|
-
(!activeOption && searchText)
|
|
363
|
-
)
|
|
364
|
-
activeOption = filteredOptions[0];
|
|
368
|
+
$: activeOption, searchText, filteredOptions, updateActiveOption();
|
|
365
369
|
|
|
366
370
|
//TODO: optimise isSelected function
|
|
367
371
|
$: isSelected = (option) => {
|
|
@@ -402,23 +406,30 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
402
406
|
if (asyncMode && searching) {
|
|
403
407
|
debouncedTriggerSearch(filter);
|
|
404
408
|
} else {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
409
|
+
if (allow_fuzzy_match) {
|
|
410
|
+
fuzzySearch(filter, [...normalisedOptions]);
|
|
411
|
+
} else {
|
|
412
|
+
filteredOptions = strictSearch(filter, [...normalisedOptions]);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function updateActiveOption() {
|
|
418
|
+
if (
|
|
419
|
+
(activeOption &&
|
|
420
|
+
searching &&
|
|
421
|
+
!filteredOptions.some(
|
|
422
|
+
(fo) => fo[used_value_key] === activeOption[used_value_key]
|
|
423
|
+
)) ||
|
|
424
|
+
(!activeOption && searchText)
|
|
425
|
+
) {
|
|
426
|
+
activeOption = filteredOptions[0];
|
|
427
|
+
} else {
|
|
428
|
+
if (allow_fuzzy_match) {
|
|
429
|
+
activeOption = filteredOptions.find((opts) =>
|
|
430
|
+
matchesValue(activeOption, opts)
|
|
431
|
+
);
|
|
432
|
+
}
|
|
422
433
|
}
|
|
423
434
|
}
|
|
424
435
|
|
|
@@ -474,6 +485,13 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
474
485
|
modifiers: [sameWidthPopperModifier],
|
|
475
486
|
});
|
|
476
487
|
|
|
488
|
+
if (allow_fuzzy_match && fuzzy) {
|
|
489
|
+
fuzzy.analyzeSubTerms = true;
|
|
490
|
+
fuzzy.analyzeSubTermDepth = 10;
|
|
491
|
+
fuzzy.highlighting.before = "";
|
|
492
|
+
fuzzy.highlighting.after = "";
|
|
493
|
+
}
|
|
494
|
+
|
|
477
495
|
//normalize value for single versus multiselect
|
|
478
496
|
if (value === null || typeof value == "undefined") {
|
|
479
497
|
value = single ? null : [];
|
|
@@ -674,16 +692,15 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
674
692
|
if (_value === null || typeof _value == "undefined") {
|
|
675
693
|
return false;
|
|
676
694
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
695
|
+
let value =
|
|
696
|
+
typeof _value === "object" && `${used_value_key}` in _value
|
|
697
|
+
? _value[used_value_key]
|
|
698
|
+
: _value;
|
|
699
|
+
return `${value}` === `${_option[used_value_key]}`;
|
|
680
700
|
};
|
|
681
701
|
|
|
682
702
|
const match = (needle, haystack) => {
|
|
683
|
-
|
|
684
|
-
return allow_fuzzy_match
|
|
685
|
-
? fuzzysearch(needle, _hayStack)
|
|
686
|
-
: _hayStack.indexOf(needle) > -1;
|
|
703
|
+
return haystack.toLowerCase().indexOf(needle) > -1;
|
|
687
704
|
};
|
|
688
705
|
|
|
689
706
|
const normaliseArraysToObjects = (arr) => {
|
|
@@ -704,4 +721,40 @@ Default value: `<span>{option[search_key] || option}</span>`
|
|
|
704
721
|
searching = false;
|
|
705
722
|
});
|
|
706
723
|
};
|
|
724
|
+
|
|
725
|
+
const fuzzySearch = debounce(searchInFuzzyMode, 200, false);
|
|
726
|
+
|
|
727
|
+
function searchInFuzzyMode(filter, options) {
|
|
728
|
+
if (!filter) {
|
|
729
|
+
filteredOptions = options;
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (options.length) {
|
|
733
|
+
let result = fuzzysearch(filter, options, {
|
|
734
|
+
search_key: used_search_key,
|
|
735
|
+
scoreThreshold,
|
|
736
|
+
});
|
|
737
|
+
filteredOptions = result;
|
|
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.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": "9e6e5f8d4dbb1e5f0a40e3dade66bdfd5f31fd4d"
|
|
33
33
|
}
|
package/styles/Select.scss
CHANGED
|
@@ -187,7 +187,7 @@ $kws-select-options-z-index: $__modal-z + 1 !default;
|
|
|
187
187
|
$color-invert: nth($pair, 2);
|
|
188
188
|
$color-light: findLightColor($color);
|
|
189
189
|
$color-dark: findDarkColor($color);
|
|
190
|
-
.searchableselect {
|
|
190
|
+
.kws-searchableselect {
|
|
191
191
|
&.is-#{$name} {
|
|
192
192
|
border-color: $color;
|
|
193
193
|
&:focus-within {
|
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 };
|