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,96 @@
1
+ import { InputChoice } from './input-choice';
2
+ import { EventChoice } from './event-choice';
3
+
4
+ /**
5
+ * Events fired by Choices behave the same as standard events. Each event is triggered on the element passed to Choices (accessible via `this.passedElement`. Arguments are accessible within the `event.detail` object.
6
+ */
7
+ export interface EventMap {
8
+ /**
9
+ * Triggered each time an item is added (programmatically or by the user).
10
+ *
11
+ * **Input types affected:** text, select-one, select-multiple
12
+ *
13
+ * Arguments: id, value, label, groupValue
14
+ */
15
+ addItem: CustomEvent<EventChoice>;
16
+
17
+ /**
18
+ * Triggered each time an item is removed (programmatically or by the user).
19
+ *
20
+ * **Input types affected:** text, select-one, select-multiple
21
+ *
22
+ * Arguments: id, value, label, groupValue
23
+ */
24
+ removeItem: CustomEvent<EventChoice | undefined>;
25
+
26
+ /**
27
+ * Triggered each time an item is highlighted.
28
+ *
29
+ * **Input types affected:** text, select-multiple
30
+ *
31
+ * Arguments: id, value, label, groupValue
32
+ */
33
+ highlightItem: CustomEvent<EventChoice | undefined>;
34
+
35
+ /**
36
+ * Triggered each time an item is unhighlighted.
37
+ *
38
+ * **Input types affected:** text, select-multiple
39
+ *
40
+ * Arguments: id, value, label, groupValue
41
+ */
42
+ unhighlightItem: CustomEvent<EventChoice | undefined>;
43
+
44
+ /**
45
+ * Triggered each time a choice is selected **by a user**, regardless if it changes the value of the input.
46
+ *
47
+ * **Input types affected:** select-one, select-multiple
48
+ *
49
+ * Arguments: choice: Choice
50
+ */
51
+ choice: CustomEvent<{ choice: InputChoice }>;
52
+
53
+ /**
54
+ * Triggered each time an item is added/removed **by a user**.
55
+ *
56
+ * **Input types affected:** text, select-one, select-multiple
57
+ *
58
+ * Arguments: value
59
+ */
60
+ change: CustomEvent<{ value: string }>;
61
+
62
+ /**
63
+ * Triggered when a user types into an input to search choices. When a search is ended, a search event with an empty value with no resultCount is triggered.
64
+ *
65
+ * **Input types affected:** select-one, select-multiple
66
+ *
67
+ * Arguments: value, resultCount
68
+ */
69
+ search: CustomEvent<{ value: string; resultCount: number }>;
70
+
71
+ /**
72
+ * Triggered when the dropdown is shown.
73
+ *
74
+ * **Input types affected:** select-one, select-multiple
75
+ *
76
+ * Arguments: -
77
+ */
78
+ showDropdown: CustomEvent<undefined>;
79
+
80
+ /**
81
+ * Triggered when the dropdown is hidden.
82
+ *
83
+ * **Input types affected:** select-one, select-multiple
84
+ *
85
+ * Arguments: -
86
+ */
87
+ hideDropdown: CustomEvent<undefined>;
88
+
89
+ /**
90
+ * Triggered when a choice from the dropdown is highlighted.
91
+ *
92
+ * Input types affected: select-one, select-multiple
93
+ * Arguments: el is the choice.passedElement that was affected.
94
+ */
95
+ highlightChoice: CustomEvent<{ el: HTMLElement }>;
96
+ }
@@ -0,0 +1 @@
1
+ export type PositionOptionsType = 'auto' | 'top' | 'bottom';
@@ -0,0 +1,12 @@
1
+ export interface SearchResult<T extends object> {
2
+ item: T;
3
+ score: number;
4
+ rank: number; // values of 0 means this item is not in the search-result set, and should be discarded
5
+ }
6
+
7
+ export interface Searcher<T extends object> {
8
+ reset(): void;
9
+ isEmptyIndex(): boolean;
10
+ index(data: T[]): void;
11
+ search(needle: string): SearchResult<T>[];
12
+ }
@@ -0,0 +1,12 @@
1
+ import { ChoiceFull } from './choice-full';
2
+ import { GroupFull } from './group-full';
3
+
4
+ export interface State {
5
+ choices: ChoiceFull[];
6
+ groups: GroupFull[];
7
+ items: ChoiceFull[];
8
+ }
9
+
10
+ export type StateChangeSet = {
11
+ [K in keyof State]: boolean;
12
+ };
@@ -0,0 +1,84 @@
1
+ import { StateChangeSet, State } from './state';
2
+ import { ChoiceFull } from './choice-full';
3
+ import { GroupFull } from './group-full';
4
+ import { ActionTypes } from './action-type';
5
+
6
+ export interface AnyAction<A extends ActionTypes = ActionTypes> {
7
+ type: A;
8
+ }
9
+
10
+ export interface StateUpdate<T> {
11
+ update: boolean;
12
+ state: T;
13
+ }
14
+
15
+ export type Reducer<T> = (state: T, action: AnyAction, context?: unknown) => StateUpdate<T>;
16
+
17
+ export type StoreListener = (changes: StateChangeSet) => void;
18
+
19
+ export interface Store {
20
+ dispatch(action: AnyAction): void;
21
+
22
+ subscribe(onChange: StoreListener): void;
23
+
24
+ withTxn(func: () => void): void;
25
+
26
+ reset(): void;
27
+
28
+ get defaultState(): State;
29
+
30
+ /**
31
+ * Get store object
32
+ */
33
+ get state(): State;
34
+
35
+ /**
36
+ * Get items from store
37
+ */
38
+ get items(): ChoiceFull[];
39
+
40
+ /**
41
+ * Get highlighted items from store
42
+ */
43
+ get highlightedActiveItems(): ChoiceFull[];
44
+
45
+ /**
46
+ * Get choices from store
47
+ */
48
+ get choices(): ChoiceFull[];
49
+
50
+ /**
51
+ * Get active choices from store
52
+ */
53
+ get activeChoices(): ChoiceFull[];
54
+
55
+ /**
56
+ * Get choices that can be searched (excluding placeholders)
57
+ */
58
+ get searchableChoices(): ChoiceFull[];
59
+
60
+ /**
61
+ * Get groups from store
62
+ */
63
+ get groups(): GroupFull[];
64
+
65
+ /**
66
+ * Get active groups from store
67
+ */
68
+ get activeGroups(): GroupFull[];
69
+
70
+ /**
71
+ * Get loading state from store
72
+ */
73
+ inTxn(): boolean;
74
+
75
+ /**
76
+ * Get single choice by it's ID
77
+ */
78
+ getChoiceById(id: number): ChoiceFull | undefined;
79
+
80
+ /**
81
+ * Get group by group id
82
+ */
83
+ getGroupById(id: number): GroupFull | undefined;
84
+ }
@@ -0,0 +1,3 @@
1
+ export interface StringPreEscaped {
2
+ readonly trusted: string;
3
+ }
@@ -0,0 +1,5 @@
1
+ export interface StringUntrusted {
2
+ readonly escaped: string;
3
+
4
+ readonly raw: string;
5
+ }
@@ -0,0 +1,66 @@
1
+ import { PassedElementType } from './passed-element-type';
2
+ import { StringPreEscaped } from './string-pre-escaped';
3
+ import { ChoiceFull } from './choice-full';
4
+ import { GroupFull } from './group-full';
5
+ // eslint-disable-next-line import/no-cycle
6
+ import { Options } from './options';
7
+ import { Types } from './types';
8
+
9
+ export type TemplateOptions = Pick<
10
+ Options,
11
+ | 'classNames'
12
+ | 'allowHTML'
13
+ | 'removeItemButtonAlignLeft'
14
+ | 'removeItemIconText'
15
+ | 'removeItemLabelText'
16
+ | 'searchEnabled'
17
+ | 'labelId'
18
+ >;
19
+
20
+ export const NoticeTypes = {
21
+ noChoices: 'no-choices',
22
+ noResults: 'no-results',
23
+ addChoice: 'add-choice',
24
+ generic: '',
25
+ } as const;
26
+ export type NoticeType = Types.ValueOf<typeof NoticeTypes>;
27
+
28
+ export type CallbackOnCreateTemplatesFn = (
29
+ template: Types.StrToEl,
30
+ escapeForTemplate: Types.EscapeForTemplateFn,
31
+ getClassNames: Types.GetClassNamesFn,
32
+ ) => Partial<Templates>;
33
+
34
+ export interface Templates {
35
+ containerOuter(
36
+ options: TemplateOptions,
37
+ dir: HTMLElement['dir'],
38
+ isSelectElement: boolean,
39
+ isSelectOneElement: boolean,
40
+ searchEnabled: boolean,
41
+ passedElementType: PassedElementType,
42
+ labelId: string,
43
+ ): HTMLDivElement;
44
+
45
+ containerInner({ classNames: { containerInner } }: TemplateOptions): HTMLDivElement;
46
+
47
+ itemList(options: TemplateOptions, isSelectOneElement: boolean): HTMLDivElement;
48
+
49
+ placeholder(options: TemplateOptions, value: StringPreEscaped | string): HTMLDivElement;
50
+
51
+ item(options: TemplateOptions, choice: ChoiceFull, removeItemButton: boolean): HTMLDivElement;
52
+
53
+ choiceList(options: TemplateOptions, isSelectOneElement: boolean): HTMLDivElement;
54
+
55
+ choiceGroup(options: TemplateOptions, group: GroupFull): HTMLDivElement;
56
+
57
+ choice(options: TemplateOptions, choice: ChoiceFull, selectText: string, groupText?: string): HTMLDivElement;
58
+
59
+ input(options: TemplateOptions, placeholderValue: string | null): HTMLInputElement;
60
+
61
+ dropdown(options: TemplateOptions): HTMLDivElement;
62
+
63
+ notice(options: TemplateOptions, innerText: string, type: NoticeType): HTMLDivElement;
64
+
65
+ option(choice: ChoiceFull): HTMLOptionElement;
66
+ }
@@ -0,0 +1,21 @@
1
+ import { StringUntrusted } from './string-untrusted';
2
+ import { StringPreEscaped } from './string-pre-escaped';
3
+
4
+ export namespace Types {
5
+ export type StrToEl = (str: string) => HTMLElement | HTMLInputElement | HTMLOptionElement;
6
+ export type EscapeForTemplateFn = (allowHTML: boolean, s: StringUntrusted | StringPreEscaped | string) => string;
7
+ export type GetClassNamesFn = (s: string | Array<string>) => string;
8
+ export type StringFunction = () => string;
9
+ export type NoticeStringFunction = (value: string, valueRaw: string) => string;
10
+ export type NoticeLimitFunction = (maxItemCount: number) => string;
11
+ export type FilterFunction = (value: string) => boolean;
12
+ export type ValueCompareFunction = (value1: string, value2: string) => boolean;
13
+
14
+ export interface RecordToCompare {
15
+ value?: StringUntrusted | string;
16
+ label?: StringUntrusted | string;
17
+ }
18
+ export type ValueOf<T extends object> = T[keyof T];
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ export type CustomProperties = Record<string, any> | string;
21
+ }
@@ -0,0 +1,88 @@
1
+ import { InputChoice } from '../interfaces/input-choice';
2
+ import { InputGroup } from '../interfaces/input-group';
3
+ import { GroupFull } from '../interfaces/group-full';
4
+ import { ChoiceFull } from '../interfaces/choice-full';
5
+ import { sanitise, unwrapStringForRaw } from './utils';
6
+
7
+ type MappedInputTypeToChoiceType<T extends string | InputChoice | InputGroup> = T extends InputGroup
8
+ ? GroupFull
9
+ : ChoiceFull;
10
+
11
+ export const coerceBool = (arg: unknown, defaultValue: boolean = true): boolean =>
12
+ typeof arg === 'undefined' ? defaultValue : !!arg;
13
+
14
+ export const stringToHtmlClass = (input: string | string[] | undefined): string[] | undefined => {
15
+ if (typeof input === 'string') {
16
+ // eslint-disable-next-line no-param-reassign
17
+ input = input.split(' ').filter((s) => s.length);
18
+ }
19
+
20
+ if (Array.isArray(input) && input.length) {
21
+ return input;
22
+ }
23
+
24
+ return undefined;
25
+ };
26
+
27
+ export const mapInputToChoice = <T extends string | InputChoice | InputGroup>(
28
+ value: T,
29
+ allowGroup: boolean,
30
+ allowRawString: boolean = true,
31
+ ): MappedInputTypeToChoiceType<T> => {
32
+ if (typeof value === 'string') {
33
+ const sanitisedValue = sanitise(value);
34
+ const userValue = allowRawString || sanitisedValue === value ? value : { escaped: sanitisedValue, raw: value };
35
+
36
+ const result: ChoiceFull = mapInputToChoice<InputChoice>(
37
+ {
38
+ value,
39
+ label: userValue,
40
+ selected: true,
41
+ },
42
+ false,
43
+ );
44
+
45
+ return result as MappedInputTypeToChoiceType<T>;
46
+ }
47
+
48
+ const groupOrChoice = value as InputChoice | InputGroup;
49
+ if ('choices' in groupOrChoice) {
50
+ if (!allowGroup) {
51
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup
52
+ throw new TypeError(`optGroup is not allowed`);
53
+ }
54
+ const group = groupOrChoice;
55
+ const choices = group.choices.map((e) => mapInputToChoice<InputChoice>(e, false));
56
+
57
+ const result: GroupFull = {
58
+ id: 0, // actual ID will be assigned during _addGroup
59
+ label: unwrapStringForRaw(group.label) || group.value,
60
+ active: !!choices.length,
61
+ disabled: !!group.disabled,
62
+ choices,
63
+ };
64
+
65
+ return result as MappedInputTypeToChoiceType<T>;
66
+ }
67
+
68
+ const choice = groupOrChoice;
69
+
70
+ const result: ChoiceFull = {
71
+ id: 0, // actual ID will be assigned during _addChoice
72
+ group: null, // actual group will be assigned during _addGroup but before _addChoice
73
+ score: 0, // used in search
74
+ rank: 0, // used in search, stable sort order
75
+ value: choice.value,
76
+ label: choice.label || choice.value,
77
+ active: coerceBool(choice.active),
78
+ selected: coerceBool(choice.selected, false),
79
+ disabled: coerceBool(choice.disabled, false),
80
+ placeholder: coerceBool(choice.placeholder, false),
81
+ highlighted: false,
82
+ labelClass: stringToHtmlClass(choice.labelClass),
83
+ labelDescription: choice.labelDescription,
84
+ customProperties: choice.customProperties,
85
+ };
86
+
87
+ return result as MappedInputTypeToChoiceType<T>;
88
+ };
@@ -0,0 +1,7 @@
1
+ export const isHtmlInputElement = (e: Element): e is HTMLInputElement => e.tagName === 'INPUT';
2
+
3
+ export const isHtmlSelectElement = (e: Element): e is HTMLSelectElement => e.tagName === 'SELECT';
4
+
5
+ export const isHtmlOption = (e: Element): e is HTMLOptionElement => e.tagName === 'OPTION';
6
+
7
+ export const isHtmlOptgroup = (e: Element): e is HTMLOptGroupElement => e.tagName === 'OPTGROUP';
@@ -0,0 +1,230 @@
1
+ import { EventTypes } from '../interfaces/event-type';
2
+ import { StringUntrusted } from '../interfaces/string-untrusted';
3
+ import { StringPreEscaped } from '../interfaces/string-pre-escaped';
4
+ import { ChoiceFull } from '../interfaces/choice-full';
5
+ import { Types } from '../interfaces/types';
6
+ import { canUseDom } from '../interfaces/build-flags';
7
+
8
+ const getRandomNumber = (min: number, max: number): number => Math.floor(Math.random() * (max - min) + min);
9
+
10
+ const generateChars = (length: number): string =>
11
+ Array.from({ length }, () => getRandomNumber(0, 36).toString(36)).join('');
12
+
13
+ export const generateId = (element: HTMLInputElement | HTMLSelectElement, prefix: string): string => {
14
+ let id = element.id || (element.name && `${element.name}-${generateChars(2)}`) || generateChars(4);
15
+ id = id.replace(/(:|\.|\[|\]|,)/g, '');
16
+ id = `${prefix}-${id}`;
17
+
18
+ return id;
19
+ };
20
+
21
+ export const getAdjacentEl = (startEl: HTMLElement, selector: string, direction = 1): HTMLElement | null => {
22
+ const prop = `${direction > 0 ? 'next' : 'previous'}ElementSibling`;
23
+
24
+ let sibling = startEl[prop];
25
+ while (sibling) {
26
+ if (sibling.matches(selector)) {
27
+ return sibling;
28
+ }
29
+ sibling = sibling[prop];
30
+ }
31
+
32
+ return null;
33
+ };
34
+
35
+ export const isScrolledIntoView = (element: HTMLElement, parent: HTMLElement, direction = 1): boolean => {
36
+ let isVisible: boolean;
37
+
38
+ if (direction > 0) {
39
+ // In view from bottom
40
+ isVisible = parent.scrollTop + parent.offsetHeight >= element.offsetTop + element.offsetHeight;
41
+ } else {
42
+ // In view from top
43
+ isVisible = element.offsetTop >= parent.scrollTop;
44
+ }
45
+
46
+ return isVisible;
47
+ };
48
+
49
+ export const sanitise = <T>(value: T | StringUntrusted | StringPreEscaped | string): T | string => {
50
+ if (typeof value !== 'string') {
51
+ if (value === null || value === undefined) {
52
+ return '';
53
+ }
54
+
55
+ if (typeof value === 'object') {
56
+ if ('raw' in value) {
57
+ return sanitise(value.raw);
58
+ }
59
+ if ('trusted' in value) {
60
+ return value.trusted;
61
+ }
62
+ }
63
+
64
+ return value;
65
+ }
66
+
67
+ return value
68
+ .replace(/&/g, '&amp;')
69
+ .replace(/>/g, '&gt;')
70
+ .replace(/</g, '&lt;')
71
+ .replace(/'/g, '&#039;')
72
+ .replace(/"/g, '&quot;');
73
+ };
74
+
75
+ export const strToEl = ((): ((str: string) => Element) => {
76
+ if (!canUseDom) {
77
+ // @ts-expect-error Do not run strToEl in non-browser environment
78
+ return (): void => {};
79
+ }
80
+ const tmpEl = document.createElement('div');
81
+
82
+ return (str): Element => {
83
+ tmpEl.innerHTML = str.trim();
84
+ const firstChild = tmpEl.children[0];
85
+
86
+ while (tmpEl.firstChild) {
87
+ tmpEl.removeChild(tmpEl.firstChild);
88
+ }
89
+
90
+ return firstChild;
91
+ };
92
+ })();
93
+
94
+ export const resolveNoticeFunction = (fn: Types.NoticeStringFunction | string, value: string): string => {
95
+ return typeof fn === 'function' ? fn(sanitise(value), value) : fn;
96
+ };
97
+
98
+ export const resolveStringFunction = (fn: Types.StringFunction | string): string => {
99
+ return typeof fn === 'function' ? fn() : fn;
100
+ };
101
+
102
+ export const unwrapStringForRaw = (s?: StringUntrusted | StringPreEscaped | string): string => {
103
+ if (typeof s === 'string') {
104
+ return s;
105
+ }
106
+
107
+ if (typeof s === 'object') {
108
+ if ('trusted' in s) {
109
+ return s.trusted;
110
+ }
111
+ if ('raw' in s) {
112
+ return s.raw;
113
+ }
114
+ }
115
+
116
+ return '';
117
+ };
118
+
119
+ export const unwrapStringForEscaped = (s?: StringUntrusted | StringPreEscaped | string): string => {
120
+ if (typeof s === 'string') {
121
+ return s;
122
+ }
123
+
124
+ if (typeof s === 'object') {
125
+ if ('escaped' in s) {
126
+ return s.escaped;
127
+ }
128
+ if ('trusted' in s) {
129
+ return s.trusted;
130
+ }
131
+ }
132
+
133
+ return '';
134
+ };
135
+
136
+ export const escapeForTemplate = (allowHTML: boolean, s: StringUntrusted | StringPreEscaped | string): string =>
137
+ allowHTML ? unwrapStringForEscaped(s) : (sanitise(s) as string);
138
+
139
+ export const setElementHtml = (
140
+ el: HTMLElement,
141
+ allowHtml: boolean,
142
+ html: StringUntrusted | StringPreEscaped | string,
143
+ ): void => {
144
+ el.innerHTML = escapeForTemplate(allowHtml, html);
145
+ };
146
+
147
+ export const sortByAlpha = (
148
+ { value, label = value }: Types.RecordToCompare,
149
+ { value: value2, label: label2 = value2 }: Types.RecordToCompare,
150
+ ): number =>
151
+ unwrapStringForRaw(label).localeCompare(unwrapStringForRaw(label2), [], {
152
+ sensitivity: 'base',
153
+ ignorePunctuation: true,
154
+ numeric: true,
155
+ });
156
+
157
+ export const sortByScore = (a: Pick<ChoiceFull, 'score'>, b: Pick<ChoiceFull, 'score'>): number => {
158
+ return a.score - b.score;
159
+ };
160
+
161
+ export const sortByRank = (a: Pick<ChoiceFull, 'rank'>, b: Pick<ChoiceFull, 'rank'>): number => {
162
+ return a.rank - b.rank;
163
+ };
164
+
165
+ export const dispatchEvent = (element: HTMLElement, type: EventTypes, customArgs: object | null = null): boolean => {
166
+ const event = new CustomEvent(type, {
167
+ detail: customArgs,
168
+ bubbles: true,
169
+ cancelable: true,
170
+ });
171
+
172
+ return element.dispatchEvent(event);
173
+ };
174
+
175
+ export const cloneObject = <T>(obj: T): T => (obj !== undefined ? JSON.parse(JSON.stringify(obj)) : undefined);
176
+
177
+ /**
178
+ * Returns an array of keys present on the first but missing on the second object
179
+ */
180
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
+ export const diff = (a: Record<string, any>, b: Record<string, any>): string[] => {
182
+ const aKeys = Object.keys(a).sort();
183
+ const bKeys = Object.keys(b).sort();
184
+
185
+ return aKeys.filter((i) => bKeys.indexOf(i) < 0);
186
+ };
187
+
188
+ export const getClassNames = (ClassNames: Array<string> | string): Array<string> => {
189
+ return Array.isArray(ClassNames) ? ClassNames : [ClassNames];
190
+ };
191
+
192
+ export const getClassNamesSelector = (option: string | Array<string> | null): string => {
193
+ if (option && Array.isArray(option)) {
194
+ return option
195
+ .map((item) => {
196
+ return `.${item}`;
197
+ })
198
+ .join('');
199
+ }
200
+
201
+ return `.${option}`;
202
+ };
203
+
204
+ export const addClassesToElement = (element: HTMLElement, className: Array<string> | string): void => {
205
+ element.classList.add(...getClassNames(className));
206
+ };
207
+
208
+ export const removeClassesFromElement = (element: HTMLElement, className: Array<string> | string): void => {
209
+ element.classList.remove(...getClassNames(className));
210
+ };
211
+
212
+ export const parseCustomProperties = (customProperties?: string): object | string => {
213
+ if (typeof customProperties !== 'undefined') {
214
+ try {
215
+ return JSON.parse(customProperties);
216
+ } catch (e) {
217
+ return customProperties;
218
+ }
219
+ }
220
+
221
+ return {};
222
+ };
223
+
224
+ export const updateClassList = (item: ChoiceFull, add: string | string[], remove: string | string[]): void => {
225
+ const { itemEl } = item;
226
+ if (itemEl) {
227
+ removeClassesFromElement(itemEl, remove);
228
+ addClassesToElement(itemEl, add);
229
+ }
230
+ };