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.
- checksums.yaml +4 -4
- data/app/assets/images/select-with-search/cross-icon.svg +6 -0
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-search-tracker.js +4 -0
- data/app/assets/javascripts/govuk_publishing_components/components/select-with-search.js +57 -0
- data/app/assets/stylesheets/govuk_publishing_components/_all_components.scss +1 -0
- data/app/assets/stylesheets/govuk_publishing_components/components/_select-with-search.scss +168 -0
- data/app/assets/stylesheets/govuk_publishing_components/components/_select.scss +6 -0
- data/app/views/govuk_publishing_components/components/_select.html.erb +22 -23
- data/app/views/govuk_publishing_components/components/_select_with_search.html.erb +14 -0
- data/app/views/govuk_publishing_components/components/docs/select.yml +11 -0
- data/app/views/govuk_publishing_components/components/docs/select_with_search.yml +196 -0
- data/lib/govuk_publishing_components/presenters/select_helper.rb +8 -5
- data/lib/govuk_publishing_components/presenters/select_with_search_helper.rb +92 -0
- data/lib/govuk_publishing_components/version.rb +1 -1
- data/lib/govuk_publishing_components.rb +1 -0
- data/node_modules/choices.js/LICENSE +21 -0
- data/node_modules/choices.js/README.md +1360 -0
- data/node_modules/choices.js/package.json +173 -0
- data/node_modules/choices.js/public/assets/scripts/choices.js +5230 -0
- data/node_modules/choices.js/public/assets/scripts/choices.min.js +2 -0
- data/node_modules/choices.js/public/assets/scripts/choices.mjs +5222 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-basic.js +4748 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-basic.min.js +2 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-basic.mjs +4740 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.js +3631 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.min.js +2 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.mjs +3623 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.js +3590 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.min.js +2 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.mjs +3582 -0
- data/node_modules/choices.js/public/assets/styles/base.css +180 -0
- data/node_modules/choices.js/public/assets/styles/base.css.map +1 -0
- data/node_modules/choices.js/public/assets/styles/base.min.css +1 -0
- data/node_modules/choices.js/public/assets/styles/choices.css +338 -0
- data/node_modules/choices.js/public/assets/styles/choices.css.map +1 -0
- data/node_modules/choices.js/public/assets/styles/choices.min.css +1 -0
- data/node_modules/choices.js/public/types/src/index.d.ts +6 -0
- data/node_modules/choices.js/public/types/src/scripts/actions/choices.d.ts +30 -0
- data/node_modules/choices.js/public/types/src/scripts/actions/groups.d.ts +8 -0
- data/node_modules/choices.js/public/types/src/scripts/actions/items.d.ts +17 -0
- data/node_modules/choices.js/public/types/src/scripts/choices.d.ts +210 -0
- data/node_modules/choices.js/public/types/src/scripts/components/container.d.ts +36 -0
- data/node_modules/choices.js/public/types/src/scripts/components/dropdown.d.ts +21 -0
- data/node_modules/choices.js/public/types/src/scripts/components/index.d.ts +7 -0
- data/node_modules/choices.js/public/types/src/scripts/components/input.d.ts +37 -0
- data/node_modules/choices.js/public/types/src/scripts/components/list.d.ts +14 -0
- data/node_modules/choices.js/public/types/src/scripts/components/wrapped-element.d.ts +21 -0
- data/node_modules/choices.js/public/types/src/scripts/components/wrapped-input.d.ts +3 -0
- data/node_modules/choices.js/public/types/src/scripts/components/wrapped-select.d.ts +20 -0
- data/node_modules/choices.js/public/types/src/scripts/constants.d.ts +1 -0
- data/node_modules/choices.js/public/types/src/scripts/defaults.d.ts +4 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/action-type.d.ts +13 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/build-flags.d.ts +11 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/choice-full.d.ts +23 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/class-names.d.ts +61 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/event-choice.d.ts +7 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/event-type.d.ts +14 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/group-full.d.ts +10 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/index.d.ts +14 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/input-choice.d.ts +15 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/input-group.d.ts +10 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/item.d.ts +17 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/keycode-map.d.ts +13 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/options.d.ts +566 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/passed-element-type.d.ts +7 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/passed-element.d.ts +95 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/position-options-type.d.ts +1 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/search.d.ts +11 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/state.d.ts +10 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/store.d.ts +64 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/string-pre-escaped.d.ts +3 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/string-untrusted.d.ts +4 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/templates.d.ts +29 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/types.d.ts +18 -0
- data/node_modules/choices.js/public/types/src/scripts/lib/choice-input.d.ts +9 -0
- data/node_modules/choices.js/public/types/src/scripts/lib/html-guard-statements.d.ts +4 -0
- data/node_modules/choices.js/public/types/src/scripts/lib/utils.d.ts +31 -0
- data/node_modules/choices.js/public/types/src/scripts/reducers/choices.d.ts +8 -0
- data/node_modules/choices.js/public/types/src/scripts/reducers/groups.d.ts +8 -0
- data/node_modules/choices.js/public/types/src/scripts/reducers/items.d.ts +9 -0
- data/node_modules/choices.js/public/types/src/scripts/search/fuse.d.ts +14 -0
- data/node_modules/choices.js/public/types/src/scripts/search/index.d.ts +3 -0
- data/node_modules/choices.js/public/types/src/scripts/search/kmp.d.ts +11 -0
- data/node_modules/choices.js/public/types/src/scripts/search/prefix-filter.d.ts +11 -0
- data/node_modules/choices.js/public/types/src/scripts/store/store.d.ts +59 -0
- data/node_modules/choices.js/public/types/src/scripts/templates.d.ts +8 -0
- data/node_modules/choices.js/src/entry.js +3 -0
- data/node_modules/choices.js/src/icons/cross-inverse.svg +1 -0
- data/node_modules/choices.js/src/icons/cross.svg +1 -0
- data/node_modules/choices.js/src/index.ts +8 -0
- data/node_modules/choices.js/src/scripts/actions/choices.ts +59 -0
- data/node_modules/choices.js/src/scripts/actions/groups.ts +14 -0
- data/node_modules/choices.js/src/scripts/actions/items.ts +34 -0
- data/node_modules/choices.js/src/scripts/choices.ts +2364 -0
- data/node_modules/choices.js/src/scripts/components/container.ts +157 -0
- data/node_modules/choices.js/src/scripts/components/dropdown.ts +50 -0
- data/node_modules/choices.js/src/scripts/components/index.ts +8 -0
- data/node_modules/choices.js/src/scripts/components/input.ts +146 -0
- data/node_modules/choices.js/src/scripts/components/list.ts +89 -0
- data/node_modules/choices.js/src/scripts/components/wrapped-element.ts +89 -0
- data/node_modules/choices.js/src/scripts/components/wrapped-input.ts +3 -0
- data/node_modules/choices.js/src/scripts/components/wrapped-select.ts +115 -0
- data/node_modules/choices.js/src/scripts/constants.ts +1 -0
- data/node_modules/choices.js/src/scripts/defaults.ts +93 -0
- data/node_modules/choices.js/src/scripts/interfaces/action-type.ts +15 -0
- data/node_modules/choices.js/src/scripts/interfaces/build-flags.ts +17 -0
- data/node_modules/choices.js/src/scripts/interfaces/choice-full.ts +30 -0
- data/node_modules/choices.js/src/scripts/interfaces/class-names.ts +61 -0
- data/node_modules/choices.js/src/scripts/interfaces/event-choice.ts +9 -0
- data/node_modules/choices.js/src/scripts/interfaces/event-type.ts +16 -0
- data/node_modules/choices.js/src/scripts/interfaces/group-full.ts +12 -0
- data/node_modules/choices.js/src/scripts/interfaces/index.ts +14 -0
- data/node_modules/choices.js/src/scripts/interfaces/input-choice.ts +17 -0
- data/node_modules/choices.js/src/scripts/interfaces/input-group.ts +11 -0
- data/node_modules/choices.js/src/scripts/interfaces/item.ts +17 -0
- data/node_modules/choices.js/src/scripts/interfaces/keycode-map.ts +13 -0
- data/node_modules/choices.js/src/scripts/interfaces/options.ts +619 -0
- data/node_modules/choices.js/src/scripts/interfaces/passed-element-type.ts +9 -0
- data/node_modules/choices.js/src/scripts/interfaces/passed-element.ts +96 -0
- data/node_modules/choices.js/src/scripts/interfaces/position-options-type.ts +1 -0
- data/node_modules/choices.js/src/scripts/interfaces/search.ts +12 -0
- data/node_modules/choices.js/src/scripts/interfaces/state.ts +12 -0
- data/node_modules/choices.js/src/scripts/interfaces/store.ts +84 -0
- data/node_modules/choices.js/src/scripts/interfaces/string-pre-escaped.ts +3 -0
- data/node_modules/choices.js/src/scripts/interfaces/string-untrusted.ts +5 -0
- data/node_modules/choices.js/src/scripts/interfaces/templates.ts +66 -0
- data/node_modules/choices.js/src/scripts/interfaces/types.ts +21 -0
- data/node_modules/choices.js/src/scripts/lib/choice-input.ts +88 -0
- data/node_modules/choices.js/src/scripts/lib/html-guard-statements.ts +7 -0
- data/node_modules/choices.js/src/scripts/lib/utils.ts +230 -0
- data/node_modules/choices.js/src/scripts/reducers/choices.ts +86 -0
- data/node_modules/choices.js/src/scripts/reducers/groups.ts +32 -0
- data/node_modules/choices.js/src/scripts/reducers/items.ts +86 -0
- data/node_modules/choices.js/src/scripts/search/fuse.ts +59 -0
- data/node_modules/choices.js/src/scripts/search/index.ts +17 -0
- data/node_modules/choices.js/src/scripts/search/kmp.ts +87 -0
- data/node_modules/choices.js/src/scripts/search/prefix-filter.ts +42 -0
- data/node_modules/choices.js/src/scripts/store/store.ts +184 -0
- data/node_modules/choices.js/src/scripts/templates.ts +409 -0
- data/node_modules/choices.js/src/styles/base.scss +189 -0
- data/node_modules/choices.js/src/styles/choices.scss +414 -0
- data/node_modules/choices.js/src/tsconfig.json +22 -0
- 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
|
+
}
|