govuk_publishing_components 58.1.1 → 58.2.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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/images/select-with-search/cross-icon.svg +6 -0
  3. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-search-tracker.js +4 -0
  4. data/app/assets/javascripts/govuk_publishing_components/components/select-with-search.js +57 -0
  5. data/app/assets/stylesheets/govuk_publishing_components/_all_components.scss +1 -0
  6. data/app/assets/stylesheets/govuk_publishing_components/components/_select-with-search.scss +168 -0
  7. data/app/assets/stylesheets/govuk_publishing_components/components/_select.scss +6 -0
  8. data/app/views/govuk_publishing_components/components/_select.html.erb +22 -23
  9. data/app/views/govuk_publishing_components/components/_select_with_search.html.erb +14 -0
  10. data/app/views/govuk_publishing_components/components/docs/select.yml +11 -0
  11. data/app/views/govuk_publishing_components/components/docs/select_with_search.yml +196 -0
  12. data/lib/govuk_publishing_components/presenters/select_helper.rb +8 -5
  13. data/lib/govuk_publishing_components/presenters/select_with_search_helper.rb +92 -0
  14. data/lib/govuk_publishing_components/version.rb +1 -1
  15. data/lib/govuk_publishing_components.rb +1 -0
  16. data/node_modules/choices.js/LICENSE +21 -0
  17. data/node_modules/choices.js/README.md +1360 -0
  18. data/node_modules/choices.js/package.json +173 -0
  19. data/node_modules/choices.js/public/assets/scripts/choices.js +5230 -0
  20. data/node_modules/choices.js/public/assets/scripts/choices.min.js +2 -0
  21. data/node_modules/choices.js/public/assets/scripts/choices.mjs +5222 -0
  22. data/node_modules/choices.js/public/assets/scripts/choices.search-basic.js +4748 -0
  23. data/node_modules/choices.js/public/assets/scripts/choices.search-basic.min.js +2 -0
  24. data/node_modules/choices.js/public/assets/scripts/choices.search-basic.mjs +4740 -0
  25. data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.js +3631 -0
  26. data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.min.js +2 -0
  27. data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.mjs +3623 -0
  28. data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.js +3590 -0
  29. data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.min.js +2 -0
  30. data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.mjs +3582 -0
  31. data/node_modules/choices.js/public/assets/styles/base.css +180 -0
  32. data/node_modules/choices.js/public/assets/styles/base.css.map +1 -0
  33. data/node_modules/choices.js/public/assets/styles/base.min.css +1 -0
  34. data/node_modules/choices.js/public/assets/styles/choices.css +338 -0
  35. data/node_modules/choices.js/public/assets/styles/choices.css.map +1 -0
  36. data/node_modules/choices.js/public/assets/styles/choices.min.css +1 -0
  37. data/node_modules/choices.js/public/types/src/index.d.ts +6 -0
  38. data/node_modules/choices.js/public/types/src/scripts/actions/choices.d.ts +30 -0
  39. data/node_modules/choices.js/public/types/src/scripts/actions/groups.d.ts +8 -0
  40. data/node_modules/choices.js/public/types/src/scripts/actions/items.d.ts +17 -0
  41. data/node_modules/choices.js/public/types/src/scripts/choices.d.ts +210 -0
  42. data/node_modules/choices.js/public/types/src/scripts/components/container.d.ts +36 -0
  43. data/node_modules/choices.js/public/types/src/scripts/components/dropdown.d.ts +21 -0
  44. data/node_modules/choices.js/public/types/src/scripts/components/index.d.ts +7 -0
  45. data/node_modules/choices.js/public/types/src/scripts/components/input.d.ts +37 -0
  46. data/node_modules/choices.js/public/types/src/scripts/components/list.d.ts +14 -0
  47. data/node_modules/choices.js/public/types/src/scripts/components/wrapped-element.d.ts +21 -0
  48. data/node_modules/choices.js/public/types/src/scripts/components/wrapped-input.d.ts +3 -0
  49. data/node_modules/choices.js/public/types/src/scripts/components/wrapped-select.d.ts +20 -0
  50. data/node_modules/choices.js/public/types/src/scripts/constants.d.ts +1 -0
  51. data/node_modules/choices.js/public/types/src/scripts/defaults.d.ts +4 -0
  52. data/node_modules/choices.js/public/types/src/scripts/interfaces/action-type.d.ts +13 -0
  53. data/node_modules/choices.js/public/types/src/scripts/interfaces/build-flags.d.ts +11 -0
  54. data/node_modules/choices.js/public/types/src/scripts/interfaces/choice-full.d.ts +23 -0
  55. data/node_modules/choices.js/public/types/src/scripts/interfaces/class-names.d.ts +61 -0
  56. data/node_modules/choices.js/public/types/src/scripts/interfaces/event-choice.d.ts +7 -0
  57. data/node_modules/choices.js/public/types/src/scripts/interfaces/event-type.d.ts +14 -0
  58. data/node_modules/choices.js/public/types/src/scripts/interfaces/group-full.d.ts +10 -0
  59. data/node_modules/choices.js/public/types/src/scripts/interfaces/index.d.ts +14 -0
  60. data/node_modules/choices.js/public/types/src/scripts/interfaces/input-choice.d.ts +15 -0
  61. data/node_modules/choices.js/public/types/src/scripts/interfaces/input-group.d.ts +10 -0
  62. data/node_modules/choices.js/public/types/src/scripts/interfaces/item.d.ts +17 -0
  63. data/node_modules/choices.js/public/types/src/scripts/interfaces/keycode-map.d.ts +13 -0
  64. data/node_modules/choices.js/public/types/src/scripts/interfaces/options.d.ts +566 -0
  65. data/node_modules/choices.js/public/types/src/scripts/interfaces/passed-element-type.d.ts +7 -0
  66. data/node_modules/choices.js/public/types/src/scripts/interfaces/passed-element.d.ts +95 -0
  67. data/node_modules/choices.js/public/types/src/scripts/interfaces/position-options-type.d.ts +1 -0
  68. data/node_modules/choices.js/public/types/src/scripts/interfaces/search.d.ts +11 -0
  69. data/node_modules/choices.js/public/types/src/scripts/interfaces/state.d.ts +10 -0
  70. data/node_modules/choices.js/public/types/src/scripts/interfaces/store.d.ts +64 -0
  71. data/node_modules/choices.js/public/types/src/scripts/interfaces/string-pre-escaped.d.ts +3 -0
  72. data/node_modules/choices.js/public/types/src/scripts/interfaces/string-untrusted.d.ts +4 -0
  73. data/node_modules/choices.js/public/types/src/scripts/interfaces/templates.d.ts +29 -0
  74. data/node_modules/choices.js/public/types/src/scripts/interfaces/types.d.ts +18 -0
  75. data/node_modules/choices.js/public/types/src/scripts/lib/choice-input.d.ts +9 -0
  76. data/node_modules/choices.js/public/types/src/scripts/lib/html-guard-statements.d.ts +4 -0
  77. data/node_modules/choices.js/public/types/src/scripts/lib/utils.d.ts +31 -0
  78. data/node_modules/choices.js/public/types/src/scripts/reducers/choices.d.ts +8 -0
  79. data/node_modules/choices.js/public/types/src/scripts/reducers/groups.d.ts +8 -0
  80. data/node_modules/choices.js/public/types/src/scripts/reducers/items.d.ts +9 -0
  81. data/node_modules/choices.js/public/types/src/scripts/search/fuse.d.ts +14 -0
  82. data/node_modules/choices.js/public/types/src/scripts/search/index.d.ts +3 -0
  83. data/node_modules/choices.js/public/types/src/scripts/search/kmp.d.ts +11 -0
  84. data/node_modules/choices.js/public/types/src/scripts/search/prefix-filter.d.ts +11 -0
  85. data/node_modules/choices.js/public/types/src/scripts/store/store.d.ts +59 -0
  86. data/node_modules/choices.js/public/types/src/scripts/templates.d.ts +8 -0
  87. data/node_modules/choices.js/src/entry.js +3 -0
  88. data/node_modules/choices.js/src/icons/cross-inverse.svg +1 -0
  89. data/node_modules/choices.js/src/icons/cross.svg +1 -0
  90. data/node_modules/choices.js/src/index.ts +8 -0
  91. data/node_modules/choices.js/src/scripts/actions/choices.ts +59 -0
  92. data/node_modules/choices.js/src/scripts/actions/groups.ts +14 -0
  93. data/node_modules/choices.js/src/scripts/actions/items.ts +34 -0
  94. data/node_modules/choices.js/src/scripts/choices.ts +2364 -0
  95. data/node_modules/choices.js/src/scripts/components/container.ts +157 -0
  96. data/node_modules/choices.js/src/scripts/components/dropdown.ts +50 -0
  97. data/node_modules/choices.js/src/scripts/components/index.ts +8 -0
  98. data/node_modules/choices.js/src/scripts/components/input.ts +146 -0
  99. data/node_modules/choices.js/src/scripts/components/list.ts +89 -0
  100. data/node_modules/choices.js/src/scripts/components/wrapped-element.ts +89 -0
  101. data/node_modules/choices.js/src/scripts/components/wrapped-input.ts +3 -0
  102. data/node_modules/choices.js/src/scripts/components/wrapped-select.ts +115 -0
  103. data/node_modules/choices.js/src/scripts/constants.ts +1 -0
  104. data/node_modules/choices.js/src/scripts/defaults.ts +93 -0
  105. data/node_modules/choices.js/src/scripts/interfaces/action-type.ts +15 -0
  106. data/node_modules/choices.js/src/scripts/interfaces/build-flags.ts +17 -0
  107. data/node_modules/choices.js/src/scripts/interfaces/choice-full.ts +30 -0
  108. data/node_modules/choices.js/src/scripts/interfaces/class-names.ts +61 -0
  109. data/node_modules/choices.js/src/scripts/interfaces/event-choice.ts +9 -0
  110. data/node_modules/choices.js/src/scripts/interfaces/event-type.ts +16 -0
  111. data/node_modules/choices.js/src/scripts/interfaces/group-full.ts +12 -0
  112. data/node_modules/choices.js/src/scripts/interfaces/index.ts +14 -0
  113. data/node_modules/choices.js/src/scripts/interfaces/input-choice.ts +17 -0
  114. data/node_modules/choices.js/src/scripts/interfaces/input-group.ts +11 -0
  115. data/node_modules/choices.js/src/scripts/interfaces/item.ts +17 -0
  116. data/node_modules/choices.js/src/scripts/interfaces/keycode-map.ts +13 -0
  117. data/node_modules/choices.js/src/scripts/interfaces/options.ts +619 -0
  118. data/node_modules/choices.js/src/scripts/interfaces/passed-element-type.ts +9 -0
  119. data/node_modules/choices.js/src/scripts/interfaces/passed-element.ts +96 -0
  120. data/node_modules/choices.js/src/scripts/interfaces/position-options-type.ts +1 -0
  121. data/node_modules/choices.js/src/scripts/interfaces/search.ts +12 -0
  122. data/node_modules/choices.js/src/scripts/interfaces/state.ts +12 -0
  123. data/node_modules/choices.js/src/scripts/interfaces/store.ts +84 -0
  124. data/node_modules/choices.js/src/scripts/interfaces/string-pre-escaped.ts +3 -0
  125. data/node_modules/choices.js/src/scripts/interfaces/string-untrusted.ts +5 -0
  126. data/node_modules/choices.js/src/scripts/interfaces/templates.ts +66 -0
  127. data/node_modules/choices.js/src/scripts/interfaces/types.ts +21 -0
  128. data/node_modules/choices.js/src/scripts/lib/choice-input.ts +88 -0
  129. data/node_modules/choices.js/src/scripts/lib/html-guard-statements.ts +7 -0
  130. data/node_modules/choices.js/src/scripts/lib/utils.ts +230 -0
  131. data/node_modules/choices.js/src/scripts/reducers/choices.ts +86 -0
  132. data/node_modules/choices.js/src/scripts/reducers/groups.ts +32 -0
  133. data/node_modules/choices.js/src/scripts/reducers/items.ts +86 -0
  134. data/node_modules/choices.js/src/scripts/search/fuse.ts +59 -0
  135. data/node_modules/choices.js/src/scripts/search/index.ts +17 -0
  136. data/node_modules/choices.js/src/scripts/search/kmp.ts +87 -0
  137. data/node_modules/choices.js/src/scripts/search/prefix-filter.ts +42 -0
  138. data/node_modules/choices.js/src/scripts/store/store.ts +184 -0
  139. data/node_modules/choices.js/src/scripts/templates.ts +409 -0
  140. data/node_modules/choices.js/src/styles/base.scss +189 -0
  141. data/node_modules/choices.js/src/styles/choices.scss +414 -0
  142. data/node_modules/choices.js/src/tsconfig.json +22 -0
  143. metadata +134 -1
@@ -0,0 +1,86 @@
1
+ /* eslint-disable */
2
+ import { ActionType, Options, State } from '../interfaces';
3
+ import { StateUpdate } from '../interfaces/store';
4
+ import { ChoiceActions } from '../actions/choices';
5
+ import { ItemActions } from '../actions/items';
6
+ import { SearchResult } from '../interfaces/search';
7
+ import { ChoiceFull } from '../interfaces/choice-full';
8
+
9
+ type ActionTypes = ChoiceActions | ItemActions;
10
+ type StateType = State['choices'];
11
+
12
+ export default function choices(s: StateType, action: ActionTypes, context?: Options): StateUpdate<StateType> {
13
+ let state = s;
14
+ let update = true;
15
+
16
+ switch (action.type) {
17
+ case ActionType.ADD_CHOICE: {
18
+ state.push(action.choice);
19
+ break;
20
+ }
21
+
22
+ case ActionType.REMOVE_CHOICE: {
23
+ action.choice.choiceEl = undefined;
24
+
25
+ if (action.choice.group) {
26
+ action.choice.group.choices = action.choice.group.choices.filter((obj) => obj.id !== action.choice.id);
27
+ }
28
+ state = state.filter((obj) => obj.id !== action.choice.id);
29
+ break;
30
+ }
31
+
32
+ case ActionType.ADD_ITEM:
33
+ case ActionType.REMOVE_ITEM: {
34
+ action.item.choiceEl = undefined;
35
+ break;
36
+ }
37
+
38
+ case ActionType.FILTER_CHOICES: {
39
+ // avoid O(n^2) algorithm complexity when searching/filtering choices
40
+ const scoreLookup: SearchResult<ChoiceFull>[] = [];
41
+ action.results.forEach((result) => {
42
+ scoreLookup[result.item.id] = result;
43
+ });
44
+
45
+ state.forEach((choice) => {
46
+ const result = scoreLookup[choice.id];
47
+ if (result !== undefined) {
48
+ choice.score = result.score;
49
+ choice.rank = result.rank;
50
+ choice.active = true;
51
+ } else {
52
+ choice.score = 0;
53
+ choice.rank = 0;
54
+ choice.active = false;
55
+ }
56
+ if (context && context.appendGroupInSearch) {
57
+ choice.choiceEl = undefined;
58
+ }
59
+ });
60
+
61
+ break;
62
+ }
63
+
64
+ case ActionType.ACTIVATE_CHOICES: {
65
+ state.forEach((choice) => {
66
+ choice.active = action.active;
67
+ if (context && context.appendGroupInSearch) {
68
+ choice.choiceEl = undefined;
69
+ }
70
+ });
71
+ break;
72
+ }
73
+
74
+ case ActionType.CLEAR_CHOICES: {
75
+ state = [];
76
+ break;
77
+ }
78
+
79
+ default: {
80
+ update = false;
81
+ break;
82
+ }
83
+ }
84
+
85
+ return { state, update };
86
+ }
@@ -0,0 +1,32 @@
1
+ import { GroupActions } from '../actions/groups';
2
+ import { State } from '../interfaces/state';
3
+ import { ActionType } from '../interfaces';
4
+ import { StateUpdate } from '../interfaces/store';
5
+ import { ChoiceActions } from '../actions/choices';
6
+
7
+ type ActionTypes = ChoiceActions | GroupActions;
8
+ type StateType = State['groups'];
9
+
10
+ export default function groups(s: StateType, action: ActionTypes): StateUpdate<StateType> {
11
+ let state = s;
12
+ let update = true;
13
+
14
+ switch (action.type) {
15
+ case ActionType.ADD_GROUP: {
16
+ state.push(action.group);
17
+ break;
18
+ }
19
+
20
+ case ActionType.CLEAR_CHOICES: {
21
+ state = [];
22
+ break;
23
+ }
24
+
25
+ default: {
26
+ update = false;
27
+ break;
28
+ }
29
+ }
30
+
31
+ return { state, update };
32
+ }
@@ -0,0 +1,86 @@
1
+ import { ItemActions } from '../actions/items';
2
+ import { State } from '../interfaces/state';
3
+ import { ChoiceActions } from '../actions/choices';
4
+ import { ActionType, Options, PassedElementTypes } from '../interfaces';
5
+ import { StateUpdate } from '../interfaces/store';
6
+ import { isHtmlSelectElement } from '../lib/html-guard-statements';
7
+ import { ChoiceFull } from '../interfaces/choice-full';
8
+ import { updateClassList } from '../lib/utils';
9
+
10
+ type ActionTypes = ChoiceActions | ItemActions;
11
+ type StateType = State['items'];
12
+
13
+ const removeItem = (item: ChoiceFull): void => {
14
+ const { itemEl } = item;
15
+ if (itemEl) {
16
+ itemEl.remove();
17
+ item.itemEl = undefined;
18
+ }
19
+ };
20
+
21
+ export default function items(s: StateType, action: ActionTypes, context?: Options): StateUpdate<StateType> {
22
+ let state = s;
23
+ let update = true;
24
+
25
+ switch (action.type) {
26
+ case ActionType.ADD_ITEM: {
27
+ action.item.selected = true;
28
+ const el = action.item.element as HTMLOptionElement | undefined;
29
+ if (el) {
30
+ el.selected = true;
31
+ el.setAttribute('selected', '');
32
+ }
33
+
34
+ state.push(action.item);
35
+ break;
36
+ }
37
+
38
+ case ActionType.REMOVE_ITEM: {
39
+ action.item.selected = false;
40
+ const el = action.item.element as HTMLOptionElement | undefined;
41
+ if (el) {
42
+ el.selected = false;
43
+ el.removeAttribute('selected');
44
+ // For a select-one, if all options are deselected, the first item is selected. To set a black value, select.value needs to be set
45
+ const select = el.parentElement;
46
+ if (select && isHtmlSelectElement(select) && select.type === PassedElementTypes.SelectOne) {
47
+ select.value = '';
48
+ }
49
+ }
50
+ // this is mixing concerns, but this is *so much faster*
51
+ removeItem(action.item);
52
+ state = state.filter((choice) => choice.id !== action.item.id);
53
+ break;
54
+ }
55
+
56
+ case ActionType.REMOVE_CHOICE: {
57
+ removeItem(action.choice);
58
+ state = state.filter((item) => item.id !== action.choice.id);
59
+ break;
60
+ }
61
+
62
+ case ActionType.HIGHLIGHT_ITEM: {
63
+ const { highlighted } = action;
64
+ const item = state.find((obj) => obj.id === action.item.id);
65
+ if (item && item.highlighted !== highlighted) {
66
+ item.highlighted = highlighted;
67
+ if (context) {
68
+ updateClassList(
69
+ item,
70
+ highlighted ? context.classNames.highlightedState : context.classNames.selectedState,
71
+ highlighted ? context.classNames.selectedState : context.classNames.highlightedState,
72
+ );
73
+ }
74
+ }
75
+
76
+ break;
77
+ }
78
+
79
+ default: {
80
+ update = false;
81
+ break;
82
+ }
83
+ }
84
+
85
+ return { state, update };
86
+ }
@@ -0,0 +1,59 @@
1
+ // eslint-disable-next-line import/no-named-default
2
+ import { default as FuseFull, IFuseOptions } from 'fuse.js';
3
+ // eslint-disable-next-line import/no-named-default
4
+ import { default as FuseBasic } from 'fuse.js/basic';
5
+ import { Options } from '../interfaces/options';
6
+ import { Searcher, SearchResult } from '../interfaces/search';
7
+ import { searchFuse } from '../interfaces/build-flags';
8
+
9
+ export class SearchByFuse<T extends object> implements Searcher<T> {
10
+ _fuseOptions: IFuseOptions<T>;
11
+
12
+ _haystack: T[] = [];
13
+
14
+ _fuse: FuseFull<T> | FuseBasic<T> | undefined;
15
+
16
+ constructor(config: Options) {
17
+ this._fuseOptions = {
18
+ ...config.fuseOptions,
19
+ keys: [...config.searchFields],
20
+ includeMatches: true,
21
+ };
22
+ }
23
+
24
+ index(data: T[]): void {
25
+ this._haystack = data;
26
+ if (this._fuse) {
27
+ this._fuse.setCollection(data);
28
+ }
29
+ }
30
+
31
+ reset(): void {
32
+ this._haystack = [];
33
+ this._fuse = undefined;
34
+ }
35
+
36
+ isEmptyIndex(): boolean {
37
+ return !this._haystack.length;
38
+ }
39
+
40
+ search(needle: string): SearchResult<T>[] {
41
+ if (!this._fuse) {
42
+ if (searchFuse === 'full') {
43
+ this._fuse = new FuseFull<T>(this._haystack, this._fuseOptions);
44
+ } else {
45
+ this._fuse = new FuseBasic<T>(this._haystack, this._fuseOptions);
46
+ }
47
+ }
48
+
49
+ const results = this._fuse.search(needle);
50
+
51
+ return results.map((value, i): SearchResult<T> => {
52
+ return {
53
+ item: value.item,
54
+ score: value.score || 0,
55
+ rank: i + 1, // If value.score is used for sorting, this can create non-stable sorts!
56
+ };
57
+ });
58
+ }
59
+ }
@@ -0,0 +1,17 @@
1
+ import { Options } from '../interfaces';
2
+ import { Searcher } from '../interfaces/search';
3
+ import { SearchByPrefixFilter } from './prefix-filter';
4
+ import { SearchByFuse } from './fuse';
5
+ import { SearchByKMP } from './kmp';
6
+ import { searchFuse, searchKMP } from '../interfaces/build-flags';
7
+
8
+ export function getSearcher<T extends object>(config: Options): Searcher<T> {
9
+ if (searchFuse && !searchKMP) {
10
+ return new SearchByFuse<T>(config);
11
+ }
12
+ if (searchKMP) {
13
+ return new SearchByKMP<T>(config);
14
+ }
15
+
16
+ return new SearchByPrefixFilter<T>(config);
17
+ }
@@ -0,0 +1,87 @@
1
+ import { Options } from '../interfaces';
2
+ import { Searcher, SearchResult } from '../interfaces/search';
3
+
4
+ function kmpSearch(pattern: string, text: string): number {
5
+ if (pattern.length === 0) {
6
+ return 0; // Immediate match
7
+ }
8
+
9
+ // Compute longest suffix-prefix table
10
+ const lsp = [0]; // Base case
11
+ for (let i = 1; i < pattern.length; i++) {
12
+ let j = lsp[i - 1]; // Start by assuming we're extending the previous LSP
13
+ while (j > 0 && pattern.charAt(i) !== pattern.charAt(j)) {
14
+ j = lsp[j - 1];
15
+ }
16
+ if (pattern.charAt(i) === pattern.charAt(j)) {
17
+ j++;
18
+ }
19
+ lsp.push(j);
20
+ }
21
+
22
+ // Walk through text string
23
+ let j = 0; // Number of chars matched in pattern
24
+ for (let i = 0; i < text.length; i++) {
25
+ while (j > 0 && text.charAt(i) !== pattern.charAt(j)) {
26
+ j = lsp[j - 1]; // Fall back in the pattern
27
+ }
28
+ if (text.charAt(i) === pattern.charAt(j)) {
29
+ j++; // Next char matched, increment position
30
+ if (j === pattern.length) {
31
+ return i - (j - 1);
32
+ }
33
+ }
34
+ }
35
+
36
+ return -1; // Not found
37
+ }
38
+
39
+ export class SearchByKMP<T extends object> implements Searcher<T> {
40
+ _fields: string[];
41
+
42
+ _haystack: T[] = [];
43
+
44
+ constructor(config: Options) {
45
+ this._fields = config.searchFields;
46
+ }
47
+
48
+ index(data: T[]): void {
49
+ this._haystack = data;
50
+ }
51
+
52
+ reset(): void {
53
+ this._haystack = [];
54
+ }
55
+
56
+ isEmptyIndex(): boolean {
57
+ return !this._haystack.length;
58
+ }
59
+
60
+ search(_needle: string): SearchResult<T>[] {
61
+ const fields = this._fields;
62
+ if (!fields || !fields.length || !_needle) {
63
+ return [];
64
+ }
65
+ const needle = _needle.toLowerCase();
66
+
67
+ const results: SearchResult<T>[] = [];
68
+
69
+ let count = 0;
70
+ for (let i = 0, j = this._haystack.length; i < j; i++) {
71
+ const obj = this._haystack[i];
72
+ for (let k = 0, l = this._fields.length; k < l; k++) {
73
+ const field = this._fields[k];
74
+ if (field in obj && kmpSearch(needle, (obj[field] as string).toLowerCase()) !== -1) {
75
+ results.push({
76
+ item: obj[field],
77
+ score: count,
78
+ rank: count + 1,
79
+ });
80
+ count++;
81
+ }
82
+ }
83
+ }
84
+
85
+ return results;
86
+ }
87
+ }
@@ -0,0 +1,42 @@
1
+ import { Options } from '../interfaces';
2
+ import { Searcher, SearchResult } from '../interfaces/search';
3
+
4
+ export class SearchByPrefixFilter<T extends object> implements Searcher<T> {
5
+ _fields: string[];
6
+
7
+ _haystack: T[] = [];
8
+
9
+ constructor(config: Options) {
10
+ this._fields = config.searchFields;
11
+ }
12
+
13
+ index(data: T[]): void {
14
+ this._haystack = data;
15
+ }
16
+
17
+ reset(): void {
18
+ this._haystack = [];
19
+ }
20
+
21
+ isEmptyIndex(): boolean {
22
+ return !this._haystack.length;
23
+ }
24
+
25
+ search(_needle: string): SearchResult<T>[] {
26
+ const fields = this._fields;
27
+ if (!fields || !fields.length || !_needle) {
28
+ return [];
29
+ }
30
+ const needle = _needle.toLowerCase();
31
+
32
+ return this._haystack
33
+ .filter((obj) => fields.some((field) => field in obj && (obj[field] as string).toLowerCase().startsWith(needle)))
34
+ .map((value, index): SearchResult<T> => {
35
+ return {
36
+ item: value,
37
+ score: index,
38
+ rank: index + 1,
39
+ };
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,184 @@
1
+ import { AnyAction, Reducer, Store as IStore, StoreListener } from '../interfaces/store';
2
+ import { StateChangeSet, State } from '../interfaces/state';
3
+ import { ChoiceFull } from '../interfaces/choice-full';
4
+ import { GroupFull } from '../interfaces/group-full';
5
+ import items from '../reducers/items';
6
+ import groups from '../reducers/groups';
7
+ import choices from '../reducers/choices';
8
+
9
+ type ReducerList = { [K in keyof State]: Reducer<State[K]> };
10
+
11
+ const reducers: ReducerList = {
12
+ groups,
13
+ items,
14
+ choices,
15
+ } as const;
16
+
17
+ export default class Store<T> implements IStore {
18
+ _state: State = this.defaultState;
19
+
20
+ _listeners: StoreListener[] = [];
21
+
22
+ _txn: number = 0;
23
+
24
+ _changeSet?: StateChangeSet;
25
+
26
+ _context: T;
27
+
28
+ constructor(context: T) {
29
+ this._context = context;
30
+ }
31
+
32
+ // eslint-disable-next-line class-methods-use-this
33
+ get defaultState(): State {
34
+ return {
35
+ groups: [],
36
+ items: [],
37
+ choices: [],
38
+ };
39
+ }
40
+
41
+ // eslint-disable-next-line class-methods-use-this
42
+ changeSet(init: boolean): StateChangeSet {
43
+ return {
44
+ groups: init,
45
+ items: init,
46
+ choices: init,
47
+ };
48
+ }
49
+
50
+ reset(): void {
51
+ this._state = this.defaultState;
52
+ const changes = this.changeSet(true);
53
+ if (this._txn) {
54
+ this._changeSet = changes;
55
+ } else {
56
+ this._listeners.forEach((l) => l(changes));
57
+ }
58
+ }
59
+
60
+ subscribe(onChange: StoreListener): this {
61
+ this._listeners.push(onChange);
62
+
63
+ return this;
64
+ }
65
+
66
+ dispatch(action: AnyAction): void {
67
+ const state = this._state;
68
+ let hasChanges = false;
69
+ const changes = this._changeSet || this.changeSet(false);
70
+
71
+ Object.keys(reducers).forEach((key: string) => {
72
+ const stateUpdate = (reducers[key] as Reducer<unknown>)(state[key], action, this._context);
73
+ if (stateUpdate.update) {
74
+ hasChanges = true;
75
+ changes[key] = true;
76
+ state[key] = stateUpdate.state;
77
+ }
78
+ });
79
+
80
+ if (hasChanges) {
81
+ if (this._txn) {
82
+ this._changeSet = changes;
83
+ } else {
84
+ this._listeners.forEach((l) => l(changes));
85
+ }
86
+ }
87
+ }
88
+
89
+ withTxn(func: () => void): void {
90
+ this._txn++;
91
+ try {
92
+ func();
93
+ } finally {
94
+ this._txn = Math.max(0, this._txn - 1);
95
+
96
+ if (!this._txn) {
97
+ const changeSet = this._changeSet;
98
+ if (changeSet) {
99
+ this._changeSet = undefined;
100
+ this._listeners.forEach((l) => l(changeSet));
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Get store object
108
+ */
109
+ get state(): State {
110
+ return this._state;
111
+ }
112
+
113
+ /**
114
+ * Get items from store
115
+ */
116
+ get items(): ChoiceFull[] {
117
+ return this.state.items;
118
+ }
119
+
120
+ /**
121
+ * Get highlighted items from store
122
+ */
123
+ get highlightedActiveItems(): ChoiceFull[] {
124
+ return this.items.filter((item) => item.active && item.highlighted);
125
+ }
126
+
127
+ /**
128
+ * Get choices from store
129
+ */
130
+ get choices(): ChoiceFull[] {
131
+ return this.state.choices;
132
+ }
133
+
134
+ /**
135
+ * Get active choices from store
136
+ */
137
+ get activeChoices(): ChoiceFull[] {
138
+ return this.choices.filter((choice) => choice.active);
139
+ }
140
+
141
+ /**
142
+ * Get choices that can be searched (excluding placeholders or disabled choices)
143
+ */
144
+ get searchableChoices(): ChoiceFull[] {
145
+ return this.choices.filter((choice) => !choice.disabled && !choice.placeholder);
146
+ }
147
+
148
+ /**
149
+ * Get groups from store
150
+ */
151
+ get groups(): GroupFull[] {
152
+ return this.state.groups;
153
+ }
154
+
155
+ /**
156
+ * Get active groups from store
157
+ */
158
+ get activeGroups(): GroupFull[] {
159
+ return this.state.groups.filter((group) => {
160
+ const isActive = group.active && !group.disabled;
161
+ const hasActiveOptions = this.state.choices.some((choice) => choice.active && !choice.disabled);
162
+
163
+ return isActive && hasActiveOptions;
164
+ }, []);
165
+ }
166
+
167
+ inTxn(): boolean {
168
+ return this._txn > 0;
169
+ }
170
+
171
+ /**
172
+ * Get single choice by it's ID
173
+ */
174
+ getChoiceById(id: number): ChoiceFull | undefined {
175
+ return this.activeChoices.find((choice) => choice.id === id);
176
+ }
177
+
178
+ /**
179
+ * Get group by group id
180
+ */
181
+ getGroupById(id: number): GroupFull | undefined {
182
+ return this.groups.find((group) => group.id === id);
183
+ }
184
+ }