@reykjavik/hanna-react 0.10.91 → 0.10.93

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.
Files changed (66) hide show
  1. package/ArticleCarousel/_ArticleCarouselCard.d.ts +2 -1
  2. package/CHANGELOG.md +23 -0
  3. package/ContactBubble.d.ts +2 -1
  4. package/ContactBubble.js +4 -6
  5. package/Datepicker.d.ts +31 -3
  6. package/Datepicker.js +25 -6
  7. package/FormField.d.ts +11 -2
  8. package/FormField.js +5 -5
  9. package/MainMenu/_PrimaryPanel.d.ts +2 -2
  10. package/MainMenu.d.ts +2 -1
  11. package/MainMenu.js +5 -6
  12. package/Multiselect/_Multiselect.search.d.ts +19 -0
  13. package/Multiselect/_Multiselect.search.js +80 -0
  14. package/Multiselect.d.ts +64 -0
  15. package/Multiselect.js +236 -0
  16. package/ReadSpeakerPlayer.d.ts +64 -0
  17. package/ReadSpeakerPlayer.js +78 -0
  18. package/RelatedLinks.d.ts +2 -1
  19. package/Selectbox.d.ts +3 -3
  20. package/TextInput.d.ts +0 -1
  21. package/_abstract/_CardList.d.ts +2 -1
  22. package/_abstract/_CardList.js +2 -2
  23. package/_abstract/_FocusTrap.d.ts +14 -0
  24. package/_abstract/_FocusTrap.js +24 -0
  25. package/_abstract/_TogglerGroup.d.ts +11 -7
  26. package/_abstract/_TogglerGroup.js +11 -3
  27. package/_abstract/_TogglerGroupField.d.ts +4 -4
  28. package/_abstract/_TogglerGroupField.js +2 -2
  29. package/_abstract/_TogglerInput.d.ts +3 -1
  30. package/_abstract/_TogglerInput.js +7 -4
  31. package/esm/ArticleCarousel/_ArticleCarouselCard.d.ts +2 -1
  32. package/esm/ContactBubble.d.ts +2 -1
  33. package/esm/ContactBubble.js +4 -6
  34. package/esm/Datepicker.d.ts +31 -3
  35. package/esm/Datepicker.js +25 -6
  36. package/esm/FormField.d.ts +11 -2
  37. package/esm/FormField.js +5 -5
  38. package/esm/MainMenu/_PrimaryPanel.d.ts +2 -2
  39. package/esm/MainMenu.d.ts +2 -1
  40. package/esm/MainMenu.js +5 -6
  41. package/esm/Multiselect/_Multiselect.search.d.ts +19 -0
  42. package/esm/Multiselect/_Multiselect.search.js +75 -0
  43. package/esm/Multiselect.d.ts +64 -0
  44. package/esm/Multiselect.js +231 -0
  45. package/esm/ReadSpeakerPlayer.d.ts +64 -0
  46. package/esm/ReadSpeakerPlayer.js +72 -0
  47. package/esm/RelatedLinks.d.ts +2 -1
  48. package/esm/Selectbox.d.ts +3 -3
  49. package/esm/TextInput.d.ts +0 -1
  50. package/esm/_abstract/_CardList.d.ts +2 -1
  51. package/esm/_abstract/_CardList.js +2 -2
  52. package/esm/_abstract/_FocusTrap.d.ts +14 -0
  53. package/esm/_abstract/_FocusTrap.js +19 -0
  54. package/esm/_abstract/_TogglerGroup.d.ts +11 -7
  55. package/esm/_abstract/_TogglerGroup.js +11 -3
  56. package/esm/_abstract/_TogglerGroupField.d.ts +4 -4
  57. package/esm/_abstract/_TogglerGroupField.js +2 -2
  58. package/esm/_abstract/_TogglerInput.d.ts +3 -1
  59. package/esm/_abstract/_TogglerInput.js +7 -4
  60. package/esm/index.d.ts +2 -0
  61. package/esm/utils/useFormatMonitor.d.ts +4 -2
  62. package/esm/utils/useFormatMonitor.js +4 -2
  63. package/index.d.ts +2 -0
  64. package/package.json +13 -5
  65. package/utils/useFormatMonitor.d.ts +4 -2
  66. package/utils/useFormatMonitor.js +4 -2
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import type { HannaColorTheme } from '@reykjavik/hanna-css';
2
3
  import { Illustration } from '@reykjavik/hanna-utils/assets';
3
4
  import { ImageProps } from '../_abstract/_Image.js';
@@ -10,7 +11,7 @@ export type ArticleCarouselCardProps = {
10
11
  title: string;
11
12
  summary: string;
12
13
  href: string;
13
- target?: string;
14
+ target?: React.HTMLAttributeAnchorTarget;
14
15
  color?: ColorFamily;
15
16
  /** NOTE: if both `color` and `theme` are specified
16
17
  * then `color` takes precedence.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@
4
4
 
5
5
  - ... <!-- Add new lines here. -->
6
6
 
7
+ ## 0.10.93
8
+
9
+ _2023-07-06_
10
+
11
+ - feat: Add component `Multiselect`
12
+ - fix: Add `target` prop to all "\*Cards" components that have `href`s
13
+ - `Datepicker`:
14
+ - feat: Add support for "uncontrolled" mode — (add prop `defaultValue`, make
15
+ `value` and `onChange` optional.)
16
+ - feat: Add prop `isoMode` to generate/submit ISO-8601 `<input/>` values
17
+ - Checkboxes and Radio buttons
18
+ - feat: Support `readOnly` — using `disabled` + `input[type=hidden]`
19
+ - feat: Support passing `options` as simple string array
20
+ - feat: Make `name` prop optional for groups
21
+ - fix: Handling of `disabled` as array of indexes
22
+
23
+ ## 0.10.92
24
+
25
+ _2023-06-06_
26
+
27
+ - feat: Add component `ReadSpeakerPlayer` and a `stopReading` helper
28
+ - fix: Update dependencies for minor esm and `Modal`-related bugfixes
29
+
7
30
  ## 0.10.90 – 0.10.91
8
31
 
9
32
  _2023-06-01_
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import { DefaultTexts } from '@reykjavik/hanna-utils/i18n';
2
3
  import { SSRSupport } from './utils.js';
3
4
  export type ContactBubbleI18n = {
@@ -25,7 +26,7 @@ export type ContactBubbleItem = {
25
26
  href: string;
26
27
  /** Prevents default link behavior unless the handler function returns `true` */
27
28
  onClick?: () => void | boolean;
28
- target?: string;
29
+ target?: React.HTMLAttributeAnchorTarget;
29
30
  } | {
30
31
  onClick: () => void | boolean;
31
32
  href?: undefined;
package/ContactBubble.js CHANGED
@@ -6,7 +6,6 @@ const react_1 = tslib_1.__importStar(require("react"));
6
6
  const focusElm_1 = require("@hugsmidjan/qj/focusElm");
7
7
  const hooks_1 = require("@hugsmidjan/react/hooks");
8
8
  const getBemClass_1 = tslib_1.__importDefault(require("@hugsmidjan/react/utils/getBemClass"));
9
- const hanna_utils_1 = require("@reykjavik/hanna-utils");
10
9
  const i18n_1 = require("@reykjavik/hanna-utils/i18n");
11
10
  const _Link_js_1 = require("./_abstract/_Link.js");
12
11
  const breakOnNL_js_1 = require("./_abstract/breakOnNL.js");
@@ -66,12 +65,11 @@ const ContactBubble = (props) => {
66
65
  wrapperElm.dataset.show = 'true';
67
66
  return;
68
67
  }
69
- const scrollElm = (0, hanna_utils_1.getPageScrollElm)();
70
68
  let pending = 0;
71
69
  const checkScroll = () => {
72
70
  if (!pending) {
73
71
  pending = requestAnimationFrame(() => {
74
- const { scrollTop, scrollHeight, clientHeight } = scrollElm;
72
+ const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
75
73
  const scrollLength = scrollHeight - clientHeight;
76
74
  // const f = scrollLength > 600 ? 1 : (scrollLength - 200) / 600;
77
75
  const f = 1;
@@ -83,7 +81,7 @@ const ContactBubble = (props) => {
83
81
  }
84
82
  };
85
83
  checkScroll();
86
- // Set scroll-listeners on both the ´document` and the `scrollElm`
84
+ // Set scroll-listeners on both the ´document` and the `document.documentElement`
87
85
  // because mobile browsers seem to handle CSS height and overflow
88
86
  // rules on <html> and <body> differently from desktop browsers.
89
87
  // Only one of these two handlers seems to trigger though,
@@ -91,10 +89,10 @@ const ContactBubble = (props) => {
91
89
  // and even if they did, the rAF throttling prevents that from
92
90
  // becoming a problem.
93
91
  document.addEventListener('scroll', checkScroll);
94
- scrollElm.addEventListener('scroll', checkScroll);
92
+ document.documentElement.addEventListener('scroll', checkScroll);
95
93
  return () => {
96
94
  document.removeEventListener('scroll', checkScroll);
97
- scrollElm.removeEventListener('scroll', checkScroll);
95
+ document.documentElement.removeEventListener('scroll', checkScroll);
98
96
  };
99
97
  }, [isBrowser, alwaysShow, closeBubble]);
100
98
  (0, react_1.useEffect)(() => {
package/Datepicker.d.ts CHANGED
@@ -4,22 +4,45 @@ export type DatepickerProps = {
4
4
  small?: boolean;
5
5
  placeholder?: string;
6
6
  value?: Date;
7
+ /**
8
+ * Default value for "uncontrolled" mode.
9
+ *
10
+ * NOTE: Even though defaultValue and the `onChange` value are both `Date`
11
+ * the `<input/>` element is still `type="text"` and it's `.value` is
12
+ * the human-readable (parsed) date `string`.
13
+ *
14
+ * Use this incombination with the `isoMode` prop to submit ISO-8601
15
+ * formatted input values
16
+ */
17
+ defaultValue?: Date;
7
18
  name?: string;
8
19
  startDate?: Date;
9
20
  endDate?: Date;
10
21
  minDate?: Date;
11
22
  maxDate?: Date;
23
+ /**
24
+ * Turn this on to generate a form <input/> that contains (and submits)
25
+ * an ISO-date formatted string value, instead of the default "human
26
+ * readable" format.
27
+ *
28
+ * NOTE: This will be the default mode in v0.11.
29
+ */
30
+ isoMode?: boolean;
12
31
  localeCode?: 'is' | 'en' | 'pl';
13
32
  dateFormat?: string | Array<string>;
14
33
  isStartDate?: boolean;
15
34
  isEndDate?: boolean;
16
35
  inputRef?: RefObject<HTMLInputElement>;
17
- onChange: (date?: Date) => void;
36
+ onChange?: (date?: Date) => void;
18
37
  datepickerExtraProps?: Record<string, unknown>;
19
- /** @deprecated use value instead. (Will be removed in v0.11) */
38
+ /** @deprecated Use `value` or `defaultValue` instead. (Will be removed in v0.11) */
20
39
  initialDate?: Date;
21
40
  } & FormFieldWrappingProps;
22
- export declare const getDateDiff: (date: Date, diff: number) => Date;
41
+ /**
42
+ * Dumb utility function that returns a new Date that's `dayOffset` days away
43
+ * from the input `date`.
44
+ */
45
+ export declare const getDateDiff: (refDate: Date, dayOffset: number) => Date;
23
46
  export type DatepickerLocaleProps = {
24
47
  nextMonthAriaLabel: string;
25
48
  nextMonthButtonLabel: string;
@@ -36,5 +59,10 @@ export type DatepickerLocaleProps = {
36
59
  chooseDayAriaLabelPrefix: string;
37
60
  disabledDayAriaLabelPrefix: string;
38
61
  };
62
+ /**
63
+ * A compo
64
+ *
65
+ * Internally, this component uses the [`react-datepicker`](https://reactdatepicker.com/) component.
66
+ */
39
67
  export declare const Datepicker: (props: DatepickerProps) => JSX.Element;
40
68
  export default Datepicker;
package/Datepicker.js CHANGED
@@ -10,11 +10,16 @@ const index_js_1 = tslib_1.__importDefault(require("date-fns/locale/is/index.js"
10
10
  const index_js_2 = tslib_1.__importDefault(require("date-fns/locale/pl/index.js"));
11
11
  const ReactDatepicker_js_1 = require("./_mixed_export_resolution_/ReactDatepicker.js"); // Docs: https://reactdatepicker.com/
12
12
  const FormField_js_1 = require("./FormField.js");
13
+ const utils_js_1 = require("./utils.js");
13
14
  (0, ReactDatepicker_js_1.registerLocale)('is', index_js_1.default);
14
15
  (0, ReactDatepicker_js_1.registerLocale)('pl', index_js_2.default);
15
- const getDateDiff = (date, diff) => {
16
- const newDate = new Date(date);
17
- newDate.setDate(newDate.getDate() + diff);
16
+ /**
17
+ * Dumb utility function that returns a new Date that's `dayOffset` days away
18
+ * from the input `date`.
19
+ */
20
+ const getDateDiff = (refDate, dayOffset) => {
21
+ const newDate = new Date(refDate);
22
+ newDate.setDate(newDate.getDate() + dayOffset);
18
23
  return newDate;
19
24
  };
20
25
  exports.getDateDiff = getDateDiff;
@@ -52,8 +57,19 @@ const i18n = {
52
57
  disabledDayAriaLabelPrefix: 'Data niedostępna',
53
58
  },
54
59
  };
60
+ /**
61
+ * A compo
62
+ *
63
+ * Internally, this component uses the [`react-datepicker`](https://reactdatepicker.com/) component.
64
+ */
55
65
  const Datepicker = (props) => {
56
- const { className, id, label, hideLabel, assistText, disabled, readOnly, invalid, errorMessage, required, reqText, placeholder, small, localeCode = 'is', dateFormat = 'd.M.yyyy', initialDate, value = initialDate, name, startDate, endDate, minDate, maxDate, isStartDate = false, isEndDate = false, onChange, datepickerExtraProps, ssr, inputRef, } = props;
66
+ const { className, id, label, hideLabel, assistText, disabled, readOnly, invalid, errorMessage, required, reqText, placeholder, small, localeCode = 'is', dateFormat = 'd.M.yyyy', name, startDate, endDate, minDate, maxDate, isStartDate = false, isEndDate = false, onChange, datepickerExtraProps, ssr, inputRef, isoMode, } = props;
67
+ const [value, setValue] = utils_js_1.useMixedControlState.raw(props.value || props.initialDate, props.defaultValue, 'value');
68
+ /*
69
+ TODO: Revert to this simpler pattern once we hit v0.11
70
+ and `props.initialDate` is removed:
71
+ */
72
+ // const [value, setValue] = useMixedControlState(props, 'value');
57
73
  const domid = (0, hooks_1.useDomid)(id);
58
74
  const txts = i18n[localeCode] || {};
59
75
  const filled = !!value;
@@ -65,7 +81,8 @@ const Datepicker = (props) => {
65
81
  (elm === null || elm === void 0 ? void 0 : elm.querySelector('input')) || undefined;
66
82
  return elm;
67
83
  }) }, addFocusProps()),
68
- react_1.default.createElement(ReactDatepicker_js_1.ReactDatePicker, Object.assign({ id: domid, required: inputProps.required, disabled: inputProps.disabled, readOnly: inputProps.readOnly, selected: value, name: name, locale: localeCode, dateFormat:
84
+ isoMode && (react_1.default.createElement("input", { type: "hidden", name: name, value: value === null || value === void 0 ? void 0 : value.toISOString().slice(0, 10) })),
85
+ react_1.default.createElement(ReactDatepicker_js_1.ReactDatePicker, Object.assign({ id: domid, required: inputProps.required, disabled: inputProps.disabled, readOnly: inputProps.readOnly, selected: value, name: isoMode ? undefined : name, locale: localeCode, dateFormat:
69
86
  // NOTE: Force all dateFormat values into Array<string> to temporarily work around
70
87
  // a bug in the current version of react-datepicker where invalid **string** values
71
88
  // are re-parsed with `new Date()`, causing surprising (weird) false positives
@@ -76,7 +93,9 @@ const Datepicker = (props) => {
76
93
  typeof dateFormat === 'string'
77
94
  ? [dateFormat]
78
95
  : dateFormat.slice(0).reverse(), onChange: (date) => {
79
- onChange(date || undefined);
96
+ date = date || undefined;
97
+ setValue(date);
98
+ onChange && onChange(date);
80
99
  const inputElm = inputRef === null || inputRef === void 0 ? void 0 : inputRef.current;
81
100
  if (inputElm) {
82
101
  inputElm.dispatchEvent(new Event('change', { bubbles: true }));
package/FormField.d.ts CHANGED
@@ -24,6 +24,10 @@ type FocusEvents = {
24
24
  onBlur?: (e: any) => void;
25
25
  };
26
26
  type FocusPropMaker = <P extends FocusEvents>(ownProps?: P) => P & Required<FocusEvents>;
27
+ /**
28
+ * Mixin base props type for components using the `FormField` to contain
29
+ * simple, singular input widgets. E.g. `TextInput`, `Seleccbox`, etc.
30
+ */
27
31
  export type FormFieldWrappingProps = {
28
32
  /** Container className - alongside "FormField" */
29
33
  className?: string;
@@ -49,14 +53,19 @@ export type FormFieldWrappingProps = {
49
53
  wrapperRef?: RefObject<HTMLElement>;
50
54
  ssr?: SSRSupport;
51
55
  };
56
+ /**
57
+ * Mixin base props type for components using `FormField` to contain
58
+ * more complex multi-element, grouped choices, that require a Heading
59
+ * E.g. `RadioGroup`, `CheckboxGroup`, etc.
60
+ */
52
61
  export type FormFieldGroupWrappingProps = FormFieldWrappingProps & {
53
62
  LabelTag?: 'h3' | 'h4' | 'h5';
54
63
  };
55
- export type FormFieldProps = FormFieldGroupWrappingProps & {
64
+ type FormFieldProps = FormFieldGroupWrappingProps & {
56
65
  /** Container className - alongside "FormField" */
57
66
  className: string;
58
67
  small?: boolean;
59
- group?: boolean;
68
+ group?: boolean | 'inputlike';
60
69
  empty?: boolean;
61
70
  filled?: boolean;
62
71
  renderInput(className: InputClassNames, inputProps: FormFieldInputProps, addFocusProps: FocusPropMaker, isBrowser?: boolean): JSX.Element;
package/FormField.js CHANGED
@@ -52,12 +52,12 @@ const FormField = (props) => {
52
52
  }
53
53
  return focusProps;
54
54
  }, []);
55
- const errorId = errorMessage ? 'error:' + domid : undefined;
56
- const assistTextId = assistText ? 'assist:' + domid : undefined;
57
- const labelId = LabelTag ? 'label:' + domid : undefined;
55
+ const errorId = errorMessage ? `error:${domid}` : undefined;
56
+ const assistTextId = assistText ? `assist:${domid}` : undefined;
57
+ const labelId = LabelTag ? `label:${domid}` : undefined;
58
58
  const reqStar = required && reqText !== false && (react_1.default.createElement("abbr", { className: "FormField__label__reqstar",
59
59
  // TODO: add mo-better i18n thinking
60
- title: (reqText || 'Þarf að fylla út') + ': ' }, "*"));
60
+ title: `${reqText || 'Þarf að fylla út'}: ` }, "*"));
61
61
  const inputProps = {
62
62
  id: domid,
63
63
  disabled: disabled,
@@ -77,7 +77,7 @@ const FormField = (props) => {
77
77
  isBrowser && filled && 'filled',
78
78
  isBrowser && focused && 'focused',
79
79
  ], className), ref: props.wrapperRef },
80
- LabelTag ? (react_1.default.createElement(LabelTag, { className: "FormField__label", id: labelId },
80
+ LabelTag ? (react_1.default.createElement(LabelTag, { className: "FormField__label", "data-inputlabel": group === 'inputlike' || undefined, id: labelId },
81
81
  ' ',
82
82
  reqStar,
83
83
  " ",
@@ -1,4 +1,4 @@
1
- import { RefObject } from 'react';
1
+ import React, { RefObject } from 'react';
2
2
  export type PrimaryPanelI18n = {
3
3
  backToMenu: string;
4
4
  backToMenuLong?: string;
@@ -9,7 +9,7 @@ export type MegaMenuItem = {
9
9
  href: string;
10
10
  lang?: string;
11
11
  current?: boolean;
12
- target?: string;
12
+ target?: React.HTMLAttributeAnchorTarget;
13
13
  };
14
14
  export type MegaMenuPanel = {
15
15
  title: string;
package/MainMenu.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import { Cleanup } from '@reykjavik/hanna-utils';
2
3
  import { DefaultTexts } from '@reykjavik/hanna-utils/i18n';
3
4
  import { AuxilaryPanelIllustration, AuxiliaryPanelProps } from './MainMenu/_Auxiliary.js';
@@ -34,7 +35,7 @@ export type MainMenuItem = {
34
35
  */
35
36
  onClick?: (index: number, item: MainMenuItem) => void | boolean;
36
37
  controlsId?: string;
37
- target?: JSX.IntrinsicElements['a']['target'];
38
+ target?: React.HTMLAttributeAnchorTarget;
38
39
  };
39
40
  export type MainMenuSeparator = '---';
40
41
  export type MainMenuItemList = Array<MainMenuItem | MainMenuSeparator>;
package/MainMenu.js CHANGED
@@ -6,7 +6,6 @@ const react_1 = tslib_1.__importStar(require("react"));
6
6
  const focusElm_1 = require("@hugsmidjan/qj/focusElm");
7
7
  const useShortState_1 = tslib_1.__importDefault(require("@hugsmidjan/react/hooks/useShortState"));
8
8
  const getBemClass_1 = tslib_1.__importDefault(require("@hugsmidjan/react/utils/getBemClass"));
9
- const hanna_utils_1 = require("@reykjavik/hanna-utils");
10
9
  const i18n_1 = require("@reykjavik/hanna-utils/i18n");
11
10
  const _Link_js_1 = require("./_abstract/_Link.js");
12
11
  const _Auxiliary_js_1 = require("./MainMenu/_Auxiliary.js");
@@ -98,20 +97,20 @@ const MainMenu = (props) => {
98
97
  const [laggyActivePanel, setLaggyActivePanel] = (0, useShortState_1.default)();
99
98
  const setActivePanel = (0, react_1.useMemo)(() => isBrowser
100
99
  ? (newActive, setFocus = true) => {
101
- const htmlElmDataset = document.documentElement.dataset;
100
+ const htmlElm = document.documentElement;
101
+ const htmlElmDataset = htmlElm.dataset;
102
102
  // const menuElm = menuElmRef.current as HTMLElement;
103
103
  _setActivePanel((activePanel) => {
104
- const scrollElm = (0, hanna_utils_1.getPageScrollElm)();
105
104
  if (!newActive) {
106
105
  activePanel && setLaggyActivePanel(activePanel, 1000);
107
- scrollElm.scrollTop = parseInt(htmlElmDataset.scrollTop || '') || 0;
106
+ htmlElm.scrollTop = parseInt(htmlElmDataset.scrollTop || '') || 0;
108
107
  delete htmlElmDataset.scrollTop;
109
108
  delete htmlElmDataset.megaPanelActive;
110
109
  }
111
110
  else {
112
111
  setLaggyActivePanel(undefined, 0);
113
- htmlElmDataset.scrollTop = String(scrollElm.scrollTop);
114
- scrollElm.scrollTop = 0;
112
+ htmlElmDataset.scrollTop = String(htmlElm.scrollTop);
113
+ htmlElm.scrollTop = 0;
115
114
  htmlElmDataset.megaPanelActive = '';
116
115
  }
117
116
  if (setFocus) {
@@ -0,0 +1,19 @@
1
+ import { TogglerGroupOptions } from '../_abstract/_TogglerGroup.js';
2
+ import { TogglerGroupFieldOption } from '../_abstract/_TogglerGroupField.js';
3
+ export declare const _weights: {
4
+ WHOLE_WORD: number;
5
+ STARTS_WITH: number;
6
+ CONTAINS: number;
7
+ VALUE_WEIGHT: number;
8
+ wordWeight: (wordIndex: number) => number;
9
+ };
10
+ export type SearchScoringfn = (
11
+ /** The Multiselect item object to calculate search score for */
12
+ item: TogglerGroupFieldOption<string>,
13
+ /** Trimmed list of `toLowerCase`d query words */
14
+ queryWords: Array<string>,
15
+ /** The raw, untouched search query as typed by the user */
16
+ rawQuery: string) => number;
17
+ export declare const defaultSearchScoring: SearchScoringfn;
18
+ /** Returns a normalized, filtered list of options */
19
+ export declare const filterItems: (options: TogglerGroupOptions<string>, searchQuery: string, searchScoringFn?: SearchScoringfn) => TogglerGroupOptions<string>;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.filterItems = exports.defaultSearchScoring = exports._weights = void 0;
4
+ const WHOLE_WORD = 10000;
5
+ const STARTS_WITH = 100;
6
+ const CONTAINS = 1;
7
+ const VALUE_WEIGHT = 1 / 10;
8
+ /** Scoring weight modifier based on a word's positional index within the value */
9
+ const wordWeight = (wordIndex) => 10 / (10 + wordIndex);
10
+ // Exported for testing purposes
11
+ exports._weights = {
12
+ WHOLE_WORD,
13
+ STARTS_WITH,
14
+ CONTAINS,
15
+ VALUE_WEIGHT,
16
+ wordWeight,
17
+ };
18
+ /**
19
+ * Calculates a score based on how well an item string (either label or value)
20
+ * matches a given list of query words.
21
+ * Splits the item string into words and scores each word.
22
+ * Favors full matches and matches at the start of the word.
23
+ * Weighs earlier words higher than words near the end.
24
+ *
25
+ * Limitation: Does currently not give extra points when query words
26
+ * appear in the correct order in the item string.
27
+ */
28
+ const calcScore = (itemString, queryWords) => {
29
+ let score = 0;
30
+ queryWords.forEach((queryWord) => {
31
+ itemString.split(/\s+/).forEach((word, wi) => {
32
+ let wordScore = 0;
33
+ if (word === queryWord) {
34
+ wordScore += WHOLE_WORD;
35
+ }
36
+ else {
37
+ const pos = word.indexOf(queryWord);
38
+ if (pos === 0) {
39
+ wordScore += STARTS_WITH;
40
+ }
41
+ else if (pos > 0) {
42
+ wordScore += CONTAINS;
43
+ }
44
+ }
45
+ score += wordScore * wordWeight(wi);
46
+ });
47
+ });
48
+ return score;
49
+ };
50
+ const defaultSearchScoring = (item, queryWords) => {
51
+ var _a;
52
+ if (!item.value) {
53
+ return 0;
54
+ }
55
+ const value = item.value.toLowerCase().trim();
56
+ const label = ((_a = item.label) === null || _a === void 0 ? void 0 : _a.toLowerCase().trim()) || value;
57
+ let score = calcScore(label, queryWords);
58
+ if (!score) {
59
+ score = VALUE_WEIGHT * calcScore(value, queryWords);
60
+ }
61
+ return score;
62
+ };
63
+ exports.defaultSearchScoring = defaultSearchScoring;
64
+ // ---------------------------------------------------------------------------
65
+ /** Returns a normalized, filtered list of options */
66
+ const filterItems = (options, searchQuery, searchScoringFn = exports.defaultSearchScoring) => {
67
+ if (!searchQuery.trim()) {
68
+ return [...options];
69
+ }
70
+ const queryWords = searchQuery.toLowerCase().trim().split(/\s+/);
71
+ return options
72
+ .map((item) => ({
73
+ item,
74
+ score: searchScoringFn(item, queryWords, searchQuery),
75
+ }))
76
+ .filter(({ score }) => score > 0)
77
+ .sort((a, b) => (a.score === b.score ? 0 : a.score < b.score ? 1 : -1))
78
+ .map(({ item }) => item);
79
+ };
80
+ exports.filterItems = filterItems;
@@ -0,0 +1,64 @@
1
+ import { TogglerGroupFieldProps } from './_abstract/_TogglerGroupField.js';
2
+ import { SearchScoringfn } from './Multiselect/_Multiselect.search.js';
3
+ export type MultiselectI18n = {
4
+ search: string;
5
+ buttonShow: string;
6
+ currentValues: string;
7
+ noneFoundMsg: string;
8
+ };
9
+ export type MultiselectProps = TogglerGroupFieldProps<string> & {
10
+ value?: Array<string>;
11
+ defaultValue?: Array<string>;
12
+ placeholder?: string;
13
+ small?: boolean;
14
+ /**
15
+ * Prevent the selected items from wrapping into multiple lines.
16
+ * Use this option when vertical space is limited.
17
+ */
18
+ nowrap?: boolean;
19
+ /**
20
+ * Custom function to calculate a search score for a given option item.
21
+ * Higher scores mean better matches.
22
+ *
23
+ * A score of zero (or less) means the item is not a valid match.
24
+ */
25
+ searchScoring?: SearchScoringfn;
26
+ /**
27
+ * Force display the current values at the top of the dropdown,
28
+ * even when the total options are fewer than
29
+ * `Multiselect.meta.summaryLimit`.
30
+ *
31
+ * NOTE: Using this option is generally not recommended.
32
+ */
33
+ forceSummary?: boolean;
34
+ /**
35
+ * Force the options to be searchable, even when they're
36
+ * fewer than `Multiselect.meta.searchableLimit`.
37
+ *
38
+ * NOTE: Using this option is generally not recommended.
39
+ */
40
+ forceSearchable?: boolean;
41
+ texts?: MultiselectI18n;
42
+ lang?: string;
43
+ };
44
+ export type MultiSelectOption = Exclude<MultiselectProps['options'][number], string>;
45
+ export declare const Multiselect: {
46
+ (props: MultiselectProps): JSX.Element;
47
+ /** Configuration constants for the Multiselect components */
48
+ meta: {
49
+ /**
50
+ * The item-count where the list becomes searchable.
51
+ *
52
+ * (The search UI, including the on-screen keyboard, takes up a lot of space
53
+ * on mobile devices, so there's a balance that we want to strike.)
54
+ */
55
+ searchableLimit: number;
56
+ /**
57
+ * The item-count above which we display a summary of "current" values
58
+ * at the top of the drop-down list.
59
+ *
60
+ * (This summary just gets in the way with ultra short option lists.)
61
+ */
62
+ summaryLimit: number;
63
+ };
64
+ };