@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 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 "fuzzy.js";
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
- let cache = {};
266
- //TODO - can optimize more for very long lists
267
- filters.forEach((word, idx) => {
268
- // iterate over each word in the search query
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
- function fuzzySearch(word, options) {
450
- let OPTS = options.map((item) => {
451
- let output = fuzzy(item.value, word);
452
- item = { ...output, ...item };
453
- item.label = output.highlightedTerm;
454
- item.score =
455
- !item.score || (item.score && item.score < output.score)
456
- ? output.score
457
- : item.score || 0;
458
- return item;
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
- let maxScore = Math.max(...OPTS.map((i) => i.score));
462
- let calculatedLimit = maxScore - scoreThreshold;
460
+ cache[idx] = opts; // storing options to current index on cache
461
+ });
462
+ setFilteredOptions(cache, filters);
463
+ }
463
464
 
464
- OPTS = OPTS.filter(
465
- (r) => r.score > (calculatedLimit > 0 ? calculatedLimit : 0)
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
- return OPTS;
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 "fuzzy.js";
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
- filteredOptions = fuzzySearch(filter, [...normalisedOptions]);
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 && searching && !filteredOptions.includes(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
- function fuzzySearch(filter, options) {
720
- if (!filter) return options;
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 OPTS = options.map((item) => {
723
- let output = fuzzy(item[used_search_key], filter);
724
- item = { ...output, original: item };
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.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": {
@@ -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": "cc856b925257a0978959fa01b32b7bea6dc52c5c"
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;
@@ -1,20 +1,42 @@
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;
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 false;
18
- }
19
- return true;
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 };