Skip to content

Commit a33c793

Browse files
refactor(core): extract utils to their own files
1 parent 0a2d0dc commit a33c793

File tree

10 files changed

+205
-199
lines changed

10 files changed

+205
-199
lines changed

packages/autocomplete-core/src/utils.ts

Lines changed: 0 additions & 199 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export function flatten<TType>(values: Array<TType | TType[]>): TType[] {
2+
return values.reduce<TType[]>((a, b) => {
3+
return a.concat(b);
4+
}, []);
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
let autocompleteId = 0;
2+
3+
export function generateAutocompleteId() {
4+
return `autocomplete-${autocompleteId++}`;
5+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { AutocompleteState } from '../types';
2+
3+
export function getItemsCount(state: AutocompleteState<any>) {
4+
if (state.collections.length === 0) {
5+
return 0;
6+
}
7+
8+
return state.collections.reduce<number>(
9+
(sum, collection) => sum + collection.items.length,
10+
0
11+
);
12+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { InternalAutocompleteOptions } from '../types';
2+
3+
export function getNextSelectedItemId<TItem>(
4+
moveAmount: number,
5+
baseIndex: number | null,
6+
itemCount: number,
7+
defaultSelectedItemId: InternalAutocompleteOptions<
8+
TItem
9+
>['defaultSelectedItemId']
10+
): number | null {
11+
// We allow circular keyboard navigation from the base index.
12+
// The base index can either be `null` (nothing is highlighted) or `0`
13+
// (the first item is highlighted).
14+
// The base index is allowed to get assigned `null` only if
15+
// `props.defaultSelectedItemId` is `null`. This pattern allows to "stop"
16+
// by the actual query before navigating to other suggestions as seen on
17+
// Google or Amazon.
18+
if (baseIndex === null && moveAmount < 0) {
19+
return itemCount - 1;
20+
}
21+
22+
if (defaultSelectedItemId !== null && baseIndex === 0 && moveAmount < 0) {
23+
return itemCount - 1;
24+
}
25+
26+
const numericIndex = (baseIndex === null ? -1 : baseIndex) + moveAmount;
27+
28+
if (numericIndex <= -1 || numericIndex >= itemCount) {
29+
return defaultSelectedItemId === null ? null : 0;
30+
}
31+
32+
return numericIndex;
33+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { MaybePromise } from '@algolia/autocomplete-shared';
2+
3+
import {
4+
AutocompleteSource,
5+
GetSourcesParams,
6+
InternalAutocompleteSource,
7+
} from '../types';
8+
9+
import { noop } from './noop';
10+
11+
function normalizeSource<TItem>(
12+
source: AutocompleteSource<TItem>
13+
): InternalAutocompleteSource<TItem> {
14+
return {
15+
getItemInputValue({ state }) {
16+
return state.query;
17+
},
18+
getItemUrl() {
19+
return undefined;
20+
},
21+
onSelect({ setIsOpen }) {
22+
setIsOpen(false);
23+
},
24+
onHighlight: noop,
25+
...source,
26+
};
27+
}
28+
29+
export function getNormalizedSources<TItem>(
30+
getSources: (
31+
params: GetSourcesParams<TItem>
32+
) => MaybePromise<Array<AutocompleteSource<TItem>>>,
33+
options: GetSourcesParams<TItem>
34+
): Promise<Array<InternalAutocompleteSource<TItem>>> {
35+
return Promise.resolve(getSources(options)).then((sources) =>
36+
Promise.all(
37+
sources.filter(Boolean).map((source) => {
38+
return Promise.resolve(normalizeSource<TItem>(source));
39+
})
40+
)
41+
);
42+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { AutocompleteCollection, AutocompleteState } from '../types';
2+
3+
// We don't have access to the autocomplete source when we call `onKeyDown`
4+
// or `onClick` because those are native browser events.
5+
// However, we can get the source from the suggestion index.
6+
function getCollectionFromSelectedItemId<TItem>({
7+
state,
8+
}: {
9+
state: AutocompleteState<TItem>;
10+
}): AutocompleteCollection<TItem> | undefined {
11+
// Given 3 sources with respectively 1, 2 and 3 suggestions: [1, 2, 3]
12+
// We want to get the accumulated counts:
13+
// [1, 1 + 2, 1 + 2 + 3] = [1, 3, 3 + 3] = [1, 3, 6]
14+
const accumulatedCollectionsCount = state.collections
15+
.map((collections) => collections.items.length)
16+
.reduce<number[]>((acc, collectionsCount, index) => {
17+
const previousValue = acc[index - 1] || 0;
18+
const nextValue = previousValue + collectionsCount;
19+
20+
acc.push(nextValue);
21+
22+
return acc;
23+
}, []);
24+
25+
// Based on the accumulated counts, we can infer the index of the suggestion.
26+
const collectionIndex = accumulatedCollectionsCount.reduce((acc, current) => {
27+
if (current <= state.selectedItemId!) {
28+
return acc + 1;
29+
}
30+
31+
return acc;
32+
}, 0);
33+
34+
return state.collections[collectionIndex];
35+
}
36+
37+
/**
38+
* Gets the highlighted index relative to a suggestion object (not the absolute
39+
* highlighted index).
40+
*
41+
* Example:
42+
* [['a', 'b'], ['c', 'd', 'e'], ['f']]
43+
* ↑
44+
* (absolute: 3, relative: 1)
45+
*/
46+
function getRelativeSelectedItemId<TItem>({
47+
state,
48+
collection,
49+
}: {
50+
state: AutocompleteState<TItem>;
51+
collection: AutocompleteCollection<TItem>;
52+
}): number {
53+
let isOffsetFound = false;
54+
let counter = 0;
55+
let previousItemsOffset = 0;
56+
57+
while (isOffsetFound === false) {
58+
const currentCollection = state.collections[counter];
59+
60+
if (currentCollection === collection) {
61+
isOffsetFound = true;
62+
break;
63+
}
64+
65+
previousItemsOffset += currentCollection.items.length;
66+
67+
counter++;
68+
}
69+
70+
return state.selectedItemId! - previousItemsOffset;
71+
}
72+
73+
export function getSelectedItem<TItem>({
74+
state,
75+
}: {
76+
state: AutocompleteState<TItem>;
77+
}) {
78+
const collection = getCollectionFromSelectedItemId({ state });
79+
80+
if (!collection) {
81+
return null;
82+
}
83+
84+
const item =
85+
collection.items[getRelativeSelectedItemId({ state, collection })];
86+
const source = collection.source;
87+
const itemInputValue = source.getItemInputValue({ item, state });
88+
const itemUrl = source.getItemUrl({ item, state });
89+
90+
return {
91+
item,
92+
itemInputValue,
93+
itemUrl,
94+
source,
95+
};
96+
}

0 commit comments

Comments
 (0)