@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 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`.
@@ -33,7 +33,7 @@
33
33
  : tracker.saved
34
34
  ? 'is-success'
35
35
  : 'is-' + color}"
36
- {disabled}
36
+ disabled={tracker.saved || tracker.saving || tracker.error || disabled}
37
37
  data-cy={cy}>
38
38
  {#if tracker.saved}
39
39
  <Icon icon="check" size={icon_size} />
@@ -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 fuzzysearch from "@kws3/ui/utils/fuzzysearch";
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
- 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
+ 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.indexOf(v) === i); // remove duplicates
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(`[${common_chars.join("")}]`, "gi");
424
+ let pattern = new RegExp(
425
+ `[${common_chars.join("").replace(/\\/g, "&#92;")}]`,
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 fuzzysearch from "@kws3/ui/utils/fuzzysearch";
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
- $: if (
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
- filteredOptions = normalisedOptions.slice().filter((item) => {
406
- // filter out items that don't match `filter`
407
- if (typeof item === "object") {
408
- if (used_search_key) {
409
- return (
410
- typeof item[used_search_key] === "string" &&
411
- match(filter, item[used_search_key])
412
- );
413
- } else {
414
- for (var key in item) {
415
- return typeof item[key] === "string" && match(filter, item[key]);
416
- }
417
- }
418
- } else {
419
- return match(filter, item);
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
- return (
678
- `${_value[used_value_key] || _value}` === `${_option[used_value_key]}`
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
- let _hayStack = haystack.toLowerCase();
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.8.4",
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": "440bdaf5e02a0a7ab0b69c2122c2c56da3485c1f"
33
+ "gitHead": "8862e5927943ef0610fe5f2f37630d977aff411f"
33
34
  }