@kws3/ui 1.9.0 → 1.9.3

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,15 @@
1
+ ## 1.9.3
2
+ - Bug fix with fuzzy.js initial config hoisting
3
+
4
+ ## 1.9.2
5
+ - Bug fix with fuzzy.js and Vite prebundling
6
+ - Debounce fuzzy searches
7
+ - Expose `@kws3/ui/utils/fuzzysearch` as a reusable function
8
+
9
+ ## 1.9.1
10
+ - `SearchableSelect` and `MultiSelect`: match colors of dropdown area to theme color when dropdown area is inside a `Portal`
11
+ - `AutoComplete`: match colors of dropdown area to theme color when dropdown area is inside a `Portal`
12
+
1
13
  ## 1.9.0
2
14
  - Use new fuzzy algorithm for `AutoComplete`,`MultiSelect` and `SearchableSelect` component
3
15
  - Add property `scoreThreshold` in `AutoComplete`,`MultiSelect` and `SearchableSelect` component to control search accuracy.
@@ -66,7 +66,9 @@ Default value: `<span>{option.label}</span>`
66
66
  class="button is-paddingless delete is-medium is-loading" />
67
67
  {/if}
68
68
  {#if rootContainer}
69
- <div class="kws-autocomplete" use:portal={dropdown_portal}>
69
+ <div
70
+ class="kws-autocomplete is-{color || 'primary'}"
71
+ use:portal={dropdown_portal}>
70
72
  <ul bind:this={dropdown} class="options" class:hidden={!show_options}>
71
73
  {#each filtered_options as option}
72
74
  <li
@@ -98,7 +100,7 @@ Default value: `<span>{option.label}</span>`
98
100
  import { debounce } from "@kws3/ui/utils";
99
101
  import { createEventDispatcher, onMount, tick } from "svelte";
100
102
  import { createPopper } from "@popperjs/core";
101
- import fuzzy from "fuzzy.js";
103
+ import { fuzzysearch } from "../utils/fuzzysearch";
102
104
 
103
105
  const sameWidthPopperModifier = {
104
106
  name: "sameWidth",
@@ -223,7 +225,8 @@ Default value: `<span>{option.label}</span>`
223
225
  filtered_options = [], //list of options filtered by search query
224
226
  normalised_options = [], //list of options normalised
225
227
  options_loading = false, //indictaes whether async search function is running
226
- mounted = false; //indicates whether component is mounted
228
+ mounted = false, //indicates whether component is mounted
229
+ fuzzyOpts = {}; // fuzzy.js lib options
227
230
 
228
231
  let list_text_size = {
229
232
  small: "7",
@@ -260,38 +263,11 @@ Default value: `<span>{option.label}</span>`
260
263
  }
261
264
 
262
265
  function triggerSearch(filters) {
263
- let cache = {};
264
- //TODO - can optimize more for very long lists
265
- filters.forEach((word, idx) => {
266
- // iterate over each word in the search query
267
- let opts = [];
268
- if (word) {
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
- }
282
- }
283
-
284
- cache[idx] = opts; // storing options to current index on cache
285
- });
286
-
287
- filtered_options = Object.values(cache) // get values from cache
288
- .flat() // flatten array
289
- .filter((v, i, self) => i === self.findIndex((t) => t.value === v.value)); // remove duplicates
290
-
291
- if (highlighted_results && !allow_fuzzy_match) {
292
- filtered_options = highlightMatches(filtered_options, filters);
266
+ if (allow_fuzzy_match) {
267
+ debouncedFuzzySearch(filters, [...normalised_options]);
268
+ } else {
269
+ searchInStrictMode(filters, [...normalised_options]);
293
270
  }
294
- setOptionsVisible(true);
295
271
  }
296
272
 
297
273
  function triggerExternalSearch(filters) {
@@ -325,14 +301,18 @@ Default value: `<span>{option.label}</span>`
325
301
  modifiers: [sameWidthPopperModifier],
326
302
  });
327
303
 
328
- if (allow_fuzzy_match && fuzzy) {
329
- fuzzy.analyzeSubTerms = true;
330
- fuzzy.analyzeSubTermDepth = 10;
331
- fuzzy.highlighting.before = "";
332
- fuzzy.highlighting.after = "";
304
+ if (allow_fuzzy_match) {
305
+ fuzzyOpts = {
306
+ analyzeSubTerms: true,
307
+ analyzeSubTermDepth: 10,
308
+ highlighting: {
309
+ after: "",
310
+ before: "",
311
+ },
312
+ };
333
313
  if (highlighted_results) {
334
- fuzzy.highlighting.before = `<span class="h">`;
335
- fuzzy.highlighting.after = "</span>";
314
+ fuzzyOpts.highlighting.before = `<span class="h">`;
315
+ fuzzyOpts.highlighting.after = "</span>";
336
316
  }
337
317
  }
338
318
 
@@ -444,25 +424,58 @@ Default value: `<span>{option.label}</span>`
444
424
  return v && v.trim() ? v.toLowerCase().trim().split(/\s+/) : [];
445
425
  }
446
426
 
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;
427
+ const debouncedFuzzySearch = debounce(searchInFuzzyMode, 200, false);
428
+
429
+ function searchInFuzzyMode(filters, options) {
430
+ let cache = {};
431
+ //TODO - can optimize more for very long lists
432
+ filters.forEach((word, idx) => {
433
+ // iterate over each word in the search query
434
+ let opts = [];
435
+ if (word) {
436
+ let result = fuzzysearch(word, options, {
437
+ search_key: "label",
438
+ scoreThreshold,
439
+ fuzzyOpts,
440
+ });
441
+ opts = result;
442
+ }
443
+
444
+ cache[idx] = opts; // storing options to current index on cache
457
445
  });
446
+ setFilteredOptions(cache);
447
+ }
458
448
 
459
- let maxScore = Math.max(...OPTS.map((i) => i.score));
460
- let calculatedLimit = maxScore - scoreThreshold;
449
+ function searchInStrictMode(filters, options) {
450
+ let cache = {};
451
+ filters.forEach((word, idx) => {
452
+ // iterate over each word in the search query
453
+ let opts = [];
454
+ if (word) {
455
+ opts = options.filter((item) => {
456
+ // filter out items that don't match `filter`
457
+ if (typeof item === "object" && item.value) {
458
+ return (
459
+ typeof item.value === "string" &&
460
+ item.value.toLowerCase().indexOf(word) > -1
461
+ );
462
+ }
463
+ });
464
+ }
461
465
 
462
- OPTS = OPTS.filter(
463
- (r) => r.score > (calculatedLimit > 0 ? calculatedLimit : 0)
464
- );
466
+ cache[idx] = opts; // storing options to current index on cache
467
+ });
468
+ setFilteredOptions(cache, filters);
469
+ }
470
+
471
+ function setFilteredOptions(cache, filters) {
472
+ filtered_options = Object.values(cache) // get values from cache
473
+ .flat() // flatten array
474
+ .filter((v, i, self) => i === self.findIndex((t) => t.value === v.value)); // remove duplicates
465
475
 
466
- return OPTS;
476
+ if (highlighted_results && !allow_fuzzy_match) {
477
+ filtered_options = highlightMatches(filtered_options, filters);
478
+ }
479
+ setOptionsVisible(true);
467
480
  }
468
481
  </script>
@@ -120,7 +120,9 @@ Default value: `<span>{option[search_key] || option}</span>`
120
120
  style={shouldShowClearAll ? "" : "display: none;"} />
121
121
  {/if}
122
122
  {#if rootContainer}
123
- <div class="kws-searchableselect" use:portal={dropdown_portal}>
123
+ <div
124
+ class="kws-searchableselect is-{color || 'primary'}"
125
+ use:portal={dropdown_portal}>
124
126
  <ul
125
127
  bind:this={dropdown}
126
128
  class="options {single ? 'is-single' : 'is-multi'}"
@@ -162,7 +164,7 @@ Default value: `<span>{option[search_key] || option}</span>`
162
164
  import { debounce } from "@kws3/ui/utils";
163
165
  import { createEventDispatcher, onMount, tick } from "svelte";
164
166
  import { createPopper } from "@popperjs/core";
165
- import fuzzy from "fuzzy.js";
167
+ import { fuzzysearch } from "../../utils/fuzzysearch";
166
168
 
167
169
  const sameWidthPopperModifier = {
168
170
  name: "sameWidth",
@@ -324,6 +326,7 @@ Default value: `<span>{option[search_key] || option}</span>`
324
326
  searchText = "",
325
327
  searching = false,
326
328
  showOptions = false,
329
+ fuzzyOpts = {}, // fuzzy.js lib options
327
330
  filteredOptions = [], //list of options filtered by search query
328
331
  normalisedOptions = [], //list of options normalised
329
332
  selectedOptions = [], //list of options that are selected
@@ -405,7 +408,7 @@ Default value: `<span>{option[search_key] || option}</span>`
405
408
  debouncedTriggerSearch(filter);
406
409
  } else {
407
410
  if (allow_fuzzy_match) {
408
- filteredOptions = fuzzySearch(filter, [...normalisedOptions]);
411
+ fuzzySearch(filter, [...normalisedOptions]);
409
412
  } else {
410
413
  filteredOptions = strictSearch(filter, [...normalisedOptions]);
411
414
  }
@@ -414,7 +417,11 @@ Default value: `<span>{option[search_key] || option}</span>`
414
417
 
415
418
  function updateActiveOption() {
416
419
  if (
417
- (activeOption && searching && !filteredOptions.includes(activeOption)) ||
420
+ (activeOption &&
421
+ searching &&
422
+ !filteredOptions.some(
423
+ (fo) => fo[used_value_key] === activeOption[used_value_key]
424
+ )) ||
418
425
  (!activeOption && searchText)
419
426
  ) {
420
427
  activeOption = filteredOptions[0];
@@ -479,9 +486,15 @@ Default value: `<span>{option[search_key] || option}</span>`
479
486
  modifiers: [sameWidthPopperModifier],
480
487
  });
481
488
 
482
- if (allow_fuzzy_match && fuzzy) {
483
- fuzzy.analyzeSubTerms = true;
484
- fuzzy.analyzeSubTermDepth = 10;
489
+ if (allow_fuzzy_match) {
490
+ fuzzyOpts = {
491
+ analyzeSubTerms: true,
492
+ analyzeSubTermDepth: 10,
493
+ highlighting: {
494
+ after: "",
495
+ before: "",
496
+ },
497
+ };
485
498
  }
486
499
 
487
500
  //normalize value for single versus multiselect
@@ -714,27 +727,20 @@ Default value: `<span>{option[search_key] || option}</span>`
714
727
  });
715
728
  };
716
729
 
717
- function fuzzySearch(filter, options) {
718
- if (!filter) return options;
730
+ const fuzzySearch = debounce(searchInFuzzyMode, 200, false);
731
+
732
+ function searchInFuzzyMode(filter, options) {
733
+ if (!filter) {
734
+ filteredOptions = options;
735
+ return;
736
+ }
719
737
  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;
738
+ let result = fuzzysearch(filter, options, {
739
+ search_key: used_search_key,
740
+ scoreThreshold,
741
+ fuzzyOpts,
728
742
  });
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;
743
+ filteredOptions = result;
738
744
  }
739
745
  }
740
746
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kws3/ui",
3
- "version": "1.9.0",
3
+ "version": "1.9.3",
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": "8862e5927943ef0610fe5f2f37630d977aff411f"
32
+ "gitHead": "c6a60f59d36f7bb3a6e4475c6c727c036d235581"
34
33
  }
@@ -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,116 @@
1
+ /**
2
+ * fuzzy.js v0.1.0
3
+ * (c) 2016 Ben Ripkens
4
+ * @license: MIT
5
+ * @params
6
+ * term : haystack
7
+ * query : needle
8
+ * opts: {
9
+ * analyzeSubTerms,
10
+ * analyzeSubTermDepth
11
+ * highlighting
12
+ * }
13
+ */
14
+ /**
15
+ *
16
+ * Adapted from fuzzy.js for @kws3/ui to work with vite prebundling
17
+ */
18
+
19
+ /*
20
+ * Whether or not fuzzy.js should analyze sub-terms, i.e. also
21
+ * check term starting positions != 0.
22
+ *
23
+ * Example:
24
+ * Given the term 'Halleluja' and query 'luja'
25
+ *
26
+ * Fuzzy.js scores this combination with an 8, when analyzeSubTerms is
27
+ * set to false, as the following matching string will be calculated:
28
+ * Ha[l]lel[uja]
29
+ *
30
+ * If you activate sub temr analysis though, the query will reach a score
31
+ * of 10, as the matching string looks as following:
32
+ * Halle[luja]
33
+ *
34
+ * Naturally, the second version is more expensive than the first one.
35
+ * You should therefore configure how many sub terms you which to analyse.
36
+ * This can be configured through opts.analyzeSubTermDepth = 10.
37
+ */
38
+
39
+ export default function fuzzy(term, query, opts = {}) {
40
+ let analyzeSubTerms = opts.analyzeSubTerms ? opts.analyzeSubTerms : false;
41
+ let analyzeSubTermDepth = opts.analyzeSubTermDepth
42
+ ? opts.analyzeSubTermDepth
43
+ : 10;
44
+ let highlighting = opts.highlighting
45
+ ? opts.highlighting
46
+ : {
47
+ before: "<em>",
48
+ after: "</em>",
49
+ };
50
+
51
+ let max = calcFuzzyScore(term, query, highlighting);
52
+ let termLength = term.length;
53
+
54
+ if (analyzeSubTerms) {
55
+ for (let i = 1; i < termLength && i < analyzeSubTermDepth; i++) {
56
+ let subTerm = term.substring(i);
57
+ let score = calcFuzzyScore(subTerm, query, highlighting);
58
+ if (score.score > max.score) {
59
+ // we need to correct 'term' and 'matchedTerm', as calcFuzzyScore
60
+ // does not now that it operates on a substring. Doing it only for
61
+ // new maximum score to save some performance.
62
+ score.term = term;
63
+ score.highlightedTerm = term.substring(0, i) + score.highlightedTerm;
64
+ max = score;
65
+ }
66
+ }
67
+ }
68
+
69
+ return max;
70
+ }
71
+
72
+ function calcFuzzyScore(term, query, highlighting) {
73
+ let score = 0;
74
+ let termLength = term.length;
75
+ let queryLength = query.length;
76
+ let _highlighting = "";
77
+ let ti = 0;
78
+ // -1 would not work as this would break the calculations of bonus
79
+ // points for subsequent character matches. Something like
80
+ // Number.MIN_VALUE would be more appropriate, but unfortunately
81
+ // Number.MIN_VALUE + 1 equals 1...
82
+ let previousMatchingCharacter = -2;
83
+
84
+ for (let qi = 0; qi < queryLength && ti < termLength; qi++) {
85
+ let qc = query.charAt(qi);
86
+ let lowerQc = qc.toLowerCase();
87
+
88
+ for (; ti < termLength; ti++) {
89
+ let tc = term.charAt(ti);
90
+
91
+ if (lowerQc === tc.toLowerCase()) {
92
+ score++;
93
+
94
+ if (previousMatchingCharacter + 1 === ti) {
95
+ score += 2;
96
+ }
97
+
98
+ _highlighting += highlighting.before + tc + highlighting.after;
99
+ previousMatchingCharacter = ti;
100
+ ti++;
101
+ break;
102
+ } else {
103
+ _highlighting += tc;
104
+ }
105
+ }
106
+ }
107
+
108
+ _highlighting += term.substring(ti, term.length);
109
+
110
+ return {
111
+ score: score,
112
+ term: term,
113
+ query: query,
114
+ highlightedTerm: _highlighting,
115
+ };
116
+ }
@@ -1,20 +1,41 @@
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
+ let fuzzyOpts = opts.fuzzyOpts ? opts.fuzzyOpts : {};
7
+
8
+ let OPTS = haystack.map((option) => {
9
+ let item = { ...option };
10
+ item.original = { ...option };
11
+ if (typeof item === "object") {
12
+ if (!Array.isArray(search_key)) {
13
+ search_key = [search_key];
15
14
  }
15
+
16
+ search_key.forEach((s_key) => {
17
+ if (`${s_key}` in item) {
18
+ let output = fuzzy(option[s_key], needle, fuzzyOpts);
19
+ item.original[s_key] = output.highlightedTerm;
20
+ item.score =
21
+ !item.score || (item.score && item.score < output.score)
22
+ ? output.score
23
+ : item.score || 0;
24
+ }
25
+ });
16
26
  }
17
- return false;
18
- }
19
- return true;
27
+ return item;
28
+ });
29
+
30
+ let maxScore = Math.max(...OPTS.map((i) => i.score));
31
+ let calculatedLimit = maxScore - scoreThreshold;
32
+
33
+ OPTS = OPTS.filter(
34
+ (r) => r.score > (calculatedLimit > 0 ? calculatedLimit : 0)
35
+ );
36
+ return OPTS.map((i) => i.original);
37
+ }
38
+
39
+ function defaultValue(opts, key, value) {
40
+ return opts && opts[key] ? opts[key] : value;
20
41
  }