From b8ff178f48438d5e5feaf2d10d7cfe6d54d4b7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 4 Nov 2020 10:25:01 +0100 Subject: [PATCH] feat(recent-searches): support search and templating (#353) BREAKING CHANGE --- examples/js/app.ts | 36 ++-- examples/js/autocomplete.css | 30 ++- examples/js/style.css | 5 - packages/autocomplete-core/package.json | 22 +-- .../autocomplete-core/src/getDefaultProps.ts | 26 ++- packages/autocomplete-core/src/types/api.ts | 22 +-- packages/autocomplete-core/src/types/index.ts | 1 + .../autocomplete-core/src/types/plugins.ts | 17 ++ packages/autocomplete-js/package.json | 20 +- packages/autocomplete-js/src/types/index.ts | 2 +- .../package.json | 32 ++- .../createLocalStorageRecentSearchesPlugin.ts | 65 +++++++ .../src/createRecentSearchesPlugin.ts | 184 +++++++++--------- .../src/createRecentSearchesStore.ts | 64 ------ .../src/createStore.ts | 31 +++ .../src/getTemplates.ts | 36 ++++ .../src/icons/index.ts | 2 + .../src/icons/recentIcon.ts | 6 + .../src/{ => icons}/resetIcon.ts | 7 +- .../src/index.ts | 4 +- .../src/recentIcon.ts | 4 - .../src/style.css | 21 -- .../src/types/MaybePromise.ts | 2 + .../src/types/RecentSearchesItem.ts | 4 + .../src/types/index.ts | 2 + .../src/usecases/localStorage/constants.ts | 3 + .../localStorage/createLocalStorage.ts | 29 +++ .../usecases/localStorage/getLocalStorage.ts | 27 +++ .../src/usecases/localStorage/index.ts | 4 + .../localStorage/isLocalStorageSupported.ts | 12 ++ .../src/usecases/localStorage/search.ts | 21 ++ .../style/index.js | 1 - .../autocomplete-preset-algolia/package.json | 20 +- 33 files changed, 473 insertions(+), 289 deletions(-) create mode 100644 packages/autocomplete-core/src/types/plugins.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/createLocalStorageRecentSearchesPlugin.ts delete mode 100644 packages/autocomplete-plugin-recent-searches/src/createRecentSearchesStore.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/createStore.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/getTemplates.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/icons/index.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/icons/recentIcon.ts rename packages/autocomplete-plugin-recent-searches/src/{ => icons}/resetIcon.ts (66%) delete mode 100644 packages/autocomplete-plugin-recent-searches/src/recentIcon.ts delete mode 100644 packages/autocomplete-plugin-recent-searches/src/style.css create mode 100644 packages/autocomplete-plugin-recent-searches/src/types/MaybePromise.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/types/RecentSearchesItem.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/types/index.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/constants.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/createLocalStorage.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/getLocalStorage.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/index.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/isLocalStorageSupported.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/search.ts delete mode 100644 packages/autocomplete-plugin-recent-searches/style/index.js diff --git a/examples/js/app.ts b/examples/js/app.ts index 50abd70e8..24e784694 100644 --- a/examples/js/app.ts +++ b/examples/js/app.ts @@ -6,8 +6,7 @@ import { getAlgoliaHits, reverseHighlightHit, } from '@algolia/autocomplete-js'; -import { createRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches'; -import '@algolia/autocomplete-plugin-recent-searches/style'; +import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches'; const searchClient = algoliasearch( 'latency', @@ -16,7 +15,10 @@ const searchClient = algoliasearch( type QuerySuggestionHit = { query: string }; -const recentSearches = createRecentSearchesPlugin({ key: 'recent' }); +const recentSearches = createLocalStorageRecentSearchesPlugin({ + key: 'recent', + limit: 3, +}); autocomplete>({ container: '#autocomplete', @@ -32,8 +34,12 @@ autocomplete>({ indexName: 'instant_search_demo_query_suggestions', query, params: { - hitsPerPage: 10, - facetFilters: [...recentSearches.data.getFacetFilters()], + hitsPerPage: recentSearches.data.getAlgoliaQuerySuggestionsHitsPerPage( + 10 + ), + facetFilters: [ + ...recentSearches.data.getAlgoliaQuerySuggestionsFacetFilters(), + ], }, }, ], @@ -49,12 +55,14 @@ autocomplete>({ templates: { item({ item }) { return ` -
${searchIcon}
-
- ${reverseHighlightHit>({ - hit: item, - attribute: 'query', - })} +
+
${searchIcon}
+
+ ${reverseHighlightHit>({ + hit: item, + attribute: 'query', + })} +
`; }, @@ -65,7 +73,8 @@ autocomplete>({ }, }); -const searchIcon = ` +const searchIcon = ` + strokeLinecap="round" strokeLinejoin="round" /> -`; + +`; diff --git a/examples/js/autocomplete.css b/examples/js/autocomplete.css index 2ca7bdbc9..94ccca054 100644 --- a/examples/js/autocomplete.css +++ b/examples/js/autocomplete.css @@ -107,11 +107,12 @@ } .aa-Item { + align-items: center; color: #23263b; cursor: pointer; - padding: 0.5rem; display: flex; - align-items: center; + grid-gap: 0.5rem; + justify-content: space-between; } .aa-Item mark { @@ -123,3 +124,28 @@ .aa-Item[aria-selected='true'] { background-color: #f5f5fa; } + +.aa-ItemContent { + display: flex; + flex-grow: 1; + grid-gap: 0.5rem; + padding: 0.5rem; +} + +.aa-ItemSourceIcon, +.aa-ItemActionButton { + color: rgba(80, 80, 80, 0.32); +} + +.aa-ItemActionButton { + background-color: transparent; + border: 0; + cursor: pointer; + opacity: 0.8; + padding: 0.5rem; + transition: color 100ms; +} + +.aa-ItemActionButton:hover { + color: rgba(80, 80, 80, 1); +} diff --git a/examples/js/style.css b/examples/js/style.css index 3029b618d..544da3288 100644 --- a/examples/js/style.css +++ b/examples/js/style.css @@ -15,8 +15,3 @@ body { padding: 1rem; width: 100%; } - -.item-icon { - color: #aaa; - padding-right: 0.5rem; -} diff --git a/packages/autocomplete-core/package.json b/packages/autocomplete-core/package.json index dfeb2e2d5..79f4c18c2 100644 --- a/packages/autocomplete-core/package.json +++ b/packages/autocomplete-core/package.json @@ -3,6 +3,12 @@ "description": "Core primitives for building autocomplete experiences.", "version": "1.0.0-alpha.34", "license": "MIT", + "homepage": "https://github.com/algolia/autocomplete.js", + "repository": "algolia/autocomplete.js", + "author": { + "name": "Algolia, Inc.", + "url": "https://www.algolia.com" + }, "source": "src/index.ts", "types": "dist/esm/index.d.ts", "module": "dist/esm/index.js", @@ -10,8 +16,10 @@ "umd:main": "dist/umd/index.js", "unpkg": "dist/umd/index.js", "jsdelivr": "dist/umd/index.js", - "homepage": "https://github.com/algolia/autocomplete.js", - "repository": "algolia/autocomplete.js", + "sideEffects": false, + "files": [ + "dist/" + ], "scripts": { "build:clean": "rm -rf ./dist", "build:esm": "babel src --root-mode upward --extensions '.ts,.tsx' --out-dir dist/esm --ignore '**/*/__tests__/'", @@ -21,13 +29,5 @@ "on:change": "concurrently \"yarn build:esm\" \"yarn build:types\"", "prepare": "yarn build:esm && yarn build:types", "watch": "watch \"yarn on:change\" --ignoreDirectoryPattern \"/dist/\"" - }, - "author": { - "name": "Algolia, Inc.", - "url": "https://www.algolia.com" - }, - "sideEffects": false, - "files": [ - "dist/" - ] + } } diff --git a/packages/autocomplete-core/src/getDefaultProps.ts b/packages/autocomplete-core/src/getDefaultProps.ts index b8df2d866..70dacd34c 100644 --- a/packages/autocomplete-core/src/getDefaultProps.ts +++ b/packages/autocomplete-core/src/getDefaultProps.ts @@ -2,7 +2,6 @@ import { InternalAutocompleteOptions, AutocompleteOptions } from './types'; import { generateAutocompleteId, getItemsCount, - noop, getNormalizedSources, flatten, } from './utils'; @@ -25,7 +24,6 @@ export function getDefaultProps( stallThreshold: 300, environment, shouldDropdownShow: ({ state }) => getItemsCount(state) > 0, - onStateChange: noop, ...props, // Since `generateAutocompleteId` triggers a side effect (it increments // and internal counter), we don't want to execute it if unnecessary. @@ -41,17 +39,19 @@ export function getDefaultProps( context: {}, ...props.initialState, }, - onSubmit: (params) => { - if (props.onSubmit) { - props.onSubmit(params); - } + onStateChange(params) { + props.onStateChange?.(params); plugins.forEach((plugin) => { - if (plugin.onSubmit) { - plugin.onSubmit(params); - } + plugin.onStateChange?.(params); + }); + }, + onSubmit(params) { + props.onSubmit?.(params); + plugins.forEach((plugin) => { + plugin.onSubmit?.(params); }); }, - getSources: (options) => { + getSources(options) { const getSourcesFromPlugins = plugins .map((plugin) => plugin.getSources) .filter((getSources) => getSources !== undefined); @@ -65,12 +65,10 @@ export function getDefaultProps( .then((sources) => sources.map((source) => ({ ...source, - onSelect: (params) => { + onSelect(params) { source.onSelect(params); plugins.forEach((plugin) => { - if (plugin.onSelect) { - plugin.onSelect(params); - } + plugin.subscribed?.onSelect?.(params); }); }, })) diff --git a/packages/autocomplete-core/src/types/api.ts b/packages/autocomplete-core/src/types/api.ts index 8ce05163d..bf791d41d 100644 --- a/packages/autocomplete-core/src/types/api.ts +++ b/packages/autocomplete-core/src/types/api.ts @@ -1,4 +1,5 @@ import { AutocompleteAccessibilityGetters } from './getters'; +import { AutocompletePlugin } from './plugins'; import { AutocompleteSetters } from './setters'; import { AutocompleteState } from './state'; import { MaybePromise } from './wrappers'; @@ -156,27 +157,6 @@ interface Navigator { }): void; } -export type AutocompletePlugin = { - /** - * The sources to get the suggestions from. - */ - getSources?( - params: GetSourcesParams - ): MaybePromise>>; - /** - * The function called when the autocomplete form is submitted. - */ - onSubmit?(params: OnSubmitParams): void; - /** - * Function called when an item is selected. - */ - onSelect?(params: OnSelectParams): void; - /** - * An extra plugin specific object to store variables and functions - */ - data?: TData; -}; - export interface AutocompleteOptions { /** * Whether to consider the experience in debug mode. diff --git a/packages/autocomplete-core/src/types/index.ts b/packages/autocomplete-core/src/types/index.ts index 31e9e79b5..abaaf911b 100644 --- a/packages/autocomplete-core/src/types/index.ts +++ b/packages/autocomplete-core/src/types/index.ts @@ -1,5 +1,6 @@ export * from './api'; export * from './getters'; +export * from './plugins'; export * from './setters'; export * from './state'; export * from './store'; diff --git a/packages/autocomplete-core/src/types/plugins.ts b/packages/autocomplete-core/src/types/plugins.ts new file mode 100644 index 000000000..5b92d5380 --- /dev/null +++ b/packages/autocomplete-core/src/types/plugins.ts @@ -0,0 +1,17 @@ +import { AutocompleteOptions, AutocompleteSource } from './api'; + +export type AutocompletePlugin = Partial< + Pick, 'onStateChange' | 'onSubmit' | 'getSources'> +> & { + /** + * The subscribed properties are properties that are called when other sources + * are interacted with. + */ + subscribed: { + onSelect: AutocompleteSource['onSelect']; + }; + /** + * An extra plugin specific object to store variables and functions + */ + data: TData; +}; diff --git a/packages/autocomplete-js/package.json b/packages/autocomplete-js/package.json index 8aa3cb636..4c0bd9e4c 100644 --- a/packages/autocomplete-js/package.json +++ b/packages/autocomplete-js/package.json @@ -3,6 +3,12 @@ "description": "Fast and fully-featured autocomplete JavaScript library.", "version": "1.0.0-alpha.34", "license": "MIT", + "homepage": "https://github.com/algolia/autocomplete.js", + "repository": "algolia/autocomplete.js", + "author": { + "name": "Algolia, Inc.", + "url": "https://www.algolia.com" + }, "source": "src/index.ts", "types": "dist/esm/index.d.ts", "module": "dist/esm/index.js", @@ -10,8 +16,10 @@ "umd:main": "dist/umd/index.js", "unpkg": "dist/umd/index.js", "jsdelivr": "dist/umd/index.js", - "homepage": "https://github.com/algolia/autocomplete.js", - "repository": "algolia/autocomplete.js", + "sideEffects": false, + "files": [ + "dist/" + ], "scripts": { "build:clean": "rm -rf ./dist", "build:esm": "babel src --root-mode upward --extensions '.ts,.tsx' --out-dir dist/esm --ignore '**/*/__tests__/'", @@ -22,14 +30,6 @@ "prepare": "yarn build:esm && yarn build:types", "watch": "watch \"yarn on:change\" --ignoreDirectoryPattern \"/dist/\"" }, - "author": { - "name": "Algolia, Inc.", - "url": "https://www.algolia.com" - }, - "sideEffects": false, - "files": [ - "dist/" - ], "dependencies": { "@algolia/autocomplete-core": "^1.0.0-alpha.34", "@algolia/autocomplete-preset-algolia": "^1.0.0-alpha.34", diff --git a/packages/autocomplete-js/src/types/index.ts b/packages/autocomplete-js/src/types/index.ts index 36b1b842e..43afdd89e 100644 --- a/packages/autocomplete-js/src/types/index.ts +++ b/packages/autocomplete-js/src/types/index.ts @@ -9,7 +9,7 @@ import { type Template = (params: TParams) => string | void; -type SourceTemplates = { +export type SourceTemplates = { /** * Templates to display in the autocomplete dropdown. * diff --git a/packages/autocomplete-plugin-recent-searches/package.json b/packages/autocomplete-plugin-recent-searches/package.json index ebd0f698a..2c11e2e56 100644 --- a/packages/autocomplete-plugin-recent-searches/package.json +++ b/packages/autocomplete-plugin-recent-searches/package.json @@ -3,6 +3,12 @@ "description": "A plugin to add recent searches to Algolia Autocomplete.", "version": "1.0.0-alpha.34", "license": "MIT", + "homepage": "https://github.com/algolia/autocomplete.js", + "repository": "algolia/autocomplete.js", + "author": { + "name": "Algolia, Inc.", + "url": "https://www.algolia.com" + }, "source": "src/index.ts", "types": "dist/esm/index.d.ts", "module": "dist/esm/index.js", @@ -10,31 +16,21 @@ "umd:main": "dist/umd/index.js", "unpkg": "dist/umd/index.js", "jsdelivr": "dist/umd/index.js", - "peerDependencies": { - "@algolia/autocomplete-core": "^1.0.0-alpha.34" - }, + "sideEffects": false, + "files": [ + "dist/" + ], "scripts": { "build:clean": "rm -rf ./dist", - "build:esm": "babel src --root-mode upward --extensions '.ts,.tsx' --out-dir dist/esm --ignore '**/*/__tests__/' && cp src/style.css ./dist/esm/", + "build:esm": "babel src --root-mode upward --extensions '.ts,.tsx' --out-dir dist/esm --ignore '**/*/__tests__/'", "build:types": "tsc -p ./tsconfig.declaration.json --outDir ./dist/esm", - "build:umd": "rollup --config && cp src/style.css ./dist/umd/", + "build:umd": "rollup --config", "build": "rm -rf ./dist && yarn build:umd && yarn build:esm && yarn build:types", "on:change": "concurrently \"yarn build:esm\" \"yarn build:types\"", "prepare": "yarn run build:esm", "watch": "watch \"yarn on:change\" --ignoreDirectoryPattern \"/dist/\"" }, - "homepage": "https://github.com/algolia/autocomplete.js", - "repository": "algolia/autocomplete.js", - "author": { - "name": "Algolia, Inc.", - "url": "https://www.algolia.com" - }, - "sideEffects": false, - "files": [ - "dist/", - "style/" - ], - "dependencies": { - "@algolia/autocomplete-core": "^1.0.0-alpha.31" + "peerDependencies": { + "@algolia/autocomplete-core": "^1.0.0-alpha.34" } } diff --git a/packages/autocomplete-plugin-recent-searches/src/createLocalStorageRecentSearchesPlugin.ts b/packages/autocomplete-plugin-recent-searches/src/createLocalStorageRecentSearchesPlugin.ts new file mode 100644 index 000000000..d591f43c5 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/createLocalStorageRecentSearchesPlugin.ts @@ -0,0 +1,65 @@ +import { AutocompletePlugin } from '@algolia/autocomplete-core'; + +import { + createRecentSearchesPlugin, + CreateRecentSearchesPluginParams, + RecentSearchesPluginData, +} from './createRecentSearchesPlugin'; +import { RecentSearchesItem } from './types'; +import { + LOCAL_STORAGE_KEY, + createLocalStorage, + search as defaultSearch, + SearchParams, +} from './usecases/localStorage'; + +export type CreateRecentSearchesLocalStorageOptions< + TItem extends RecentSearchesItem +> = { + /** + * The unique key to name the store of recent searches. + * + * @example "top_searchbar" + */ + key: string; + + /** + * The number of recent searches to store. + * + * @default 5 + */ + limit?: number; + + /** + * Function to search in the recent items. + */ + search?(params: SearchParams): TItem[]; +}; + +type LocalStorageRecentSearchesPluginOptions< + TItem extends RecentSearchesItem +> = CreateRecentSearchesPluginParams & + CreateRecentSearchesLocalStorageOptions; + +export function createLocalStorageRecentSearchesPlugin< + TItem extends RecentSearchesItem +>({ + key, + limit = 5, + getTemplates, + search = defaultSearch, +}: LocalStorageRecentSearchesPluginOptions): AutocompletePlugin< + TItem, + RecentSearchesPluginData +> { + const storage = createLocalStorage({ + key: [LOCAL_STORAGE_KEY, key].join(':'), + limit, + search, + }); + + return createRecentSearchesPlugin({ + getTemplates, + storage, + }); +} diff --git a/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesPlugin.ts b/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesPlugin.ts index 7ef3c30c3..e6ff01a57 100644 --- a/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesPlugin.ts +++ b/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesPlugin.ts @@ -1,114 +1,118 @@ import { AutocompletePlugin } from '@algolia/autocomplete-core'; +import { SourceTemplates } from '@algolia/autocomplete-js'; -import { createRecentSearchesStore } from './createRecentSearchesStore'; -import { recentIcon } from './recentIcon'; -import { resetIcon } from './resetIcon'; +import { createStore, RecentSearchesStorage } from './createStore'; +import { + getTemplates as defaultGetTemplates, + GetTemplatesParams, +} from './getTemplates'; +import { MaybePromise, RecentSearchesItem } from './types'; -type PluginOptions = { - /** - * The number of searches to store. - * - * @default 5 - */ - limit?: number; - - /** - * The key to distinguish multiple stores of recent searches. - * - * @example - * // 'top_searchbar' - */ - key: string; +type Ref = { + current: TType; }; -type RecentSearchItem = { - objectID: string; - query: string; +export type RecentSearchesPluginData = { + getAlgoliaQuerySuggestionsFacetFilters(): string[][]; + getAlgoliaQuerySuggestionsHitsPerPage(hitsPerPage: number): number; }; -type PluginData = { - getFacetFilters: () => string[]; +export type CreateRecentSearchesPluginParams< + TItem extends RecentSearchesItem +> = { + storage: RecentSearchesStorage; + getTemplates?( + params: GetTemplatesParams + ): SourceTemplates['templates']; }; -export function createRecentSearchesPlugin({ - key, - limit = 5, -}: PluginOptions): AutocompletePlugin { - const store = createRecentSearchesStore({ - key: ['AUTOCOMPLETE_RECENT_SEARCHES', key].join('__'), - limit, - }); +export function createRecentSearchesPlugin({ + storage, + getTemplates = defaultGetTemplates, +}: CreateRecentSearchesPluginParams): AutocompletePlugin< + TItem, + RecentSearchesPluginData +> { + const store = createStore(storage); + const lastItemsRef: Ref> = { current: [] }; return { - getSources: ({ query, refresh }) => { - if (query) { - return []; - } - - return [ - { - getItemInputValue: ({ item }) => item.query, - getItems() { - return store.getAll(); - }, - templates: { - item({ item, root }) { - const container = document.createElement('div'); - container.className = 'aa-RecentSearchesItem'; - - const leftItems = document.createElement('div'); - leftItems.className = 'leftItems'; - const icon = document.createElement('div'); - icon.className = 'item-icon icon'; - icon.innerHTML = recentIcon; - const title = document.createElement('div'); - title.className = 'title'; - title.innerText = item.query; - leftItems.appendChild(icon); - leftItems.appendChild(title); + subscribed: { + onSelect({ item, state, source }) { + const inputValue = source.getItemInputValue({ item, state }); - const removeButton = document.createElement('button'); - removeButton.className = 'item-icon removeButton'; - removeButton.type = 'button'; - removeButton.innerHTML = resetIcon; - removeButton.title = 'Remove'; - - container.appendChild(leftItems); - container.appendChild(removeButton); - root.appendChild(container); - - removeButton.addEventListener('click', (event) => { - event.stopPropagation(); - store.remove(item); - refresh(); - }); - }, - }, - }, - ]; + if (inputValue) { + store.add({ + id: inputValue, + query: inputValue, + } as TItem); + } + }, }, - onSubmit: ({ state }) => { + onSubmit({ state }) { const { query } = state; + if (query) { store.add({ - objectID: query, + id: query, query, - }); + } as TItem); } }, - onSelect: ({ item, state, source }) => { - const inputValue = source.getItemInputValue({ item, state }); - const { objectID } = item as any; - if (inputValue) { - store.add({ - objectID: objectID || inputValue, - query: inputValue, - }); - } + getSources({ query, refresh }) { + lastItemsRef.current = store.getAll(query); + + return Promise.resolve(lastItemsRef.current).then((items) => { + if (items.length === 0) { + return []; + } + + return [ + { + getItemInputValue({ item }) { + return item.query; + }, + getItems() { + return items; + }, + templates: getTemplates({ + onRemove(id) { + store.remove(id); + refresh(); + }, + }), + }, + ]; + }); }, data: { - getFacetFilters: () => { - return store.getAll().map((item) => [`objectID:-${item.query}`]); + getAlgoliaQuerySuggestionsFacetFilters() { + // If the items returned by `store.getAll` are contained in a Promise, + // we cannot provide the facet filters in time when this function is called + // because we need to resolve the promise before getting the value. + if (!Array.isArray(lastItemsRef.current)) { + // @TODO: use the `warn` function from `autocomplete-core` + console.warn( + 'The `getAlgoliaQuerySuggestionsFacetFilters` function is not supported with storages that return promises in `getAll`.' + ); + return []; + } + + return lastItemsRef.current.map((item) => [`objectID:-${item.query}`]); + }, + getAlgoliaQuerySuggestionsHitsPerPage(hitsPerPage: number) { + // If the items returned by `store.getAll` are contained in a Promise, + // we cannot provide the number of hits per page in time when this function + // is called because we need to resolve the promise before getting the value. + if (!Array.isArray(lastItemsRef.current)) { + // @TODO: use the `warn` function from `autocomplete-core` + console.warn( + 'The `getAlgoliaQuerySuggestionsHitsPerPage` function is not supported with storages that return promises in `getAll`.' + ); + return hitsPerPage; + } + + return Math.max(1, hitsPerPage - lastItemsRef.current.length); }, }, }; diff --git a/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesStore.ts b/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesStore.ts deleted file mode 100644 index 8a31cfce3..000000000 --- a/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesStore.ts +++ /dev/null @@ -1,64 +0,0 @@ -function isLocalStorageSupported() { - const key = '__TEST_KEY__'; - - try { - localStorage.setItem(key, ''); - localStorage.removeItem(key); - - return true; - } catch (error) { - return false; - } -} - -function createStorage(key) { - if (isLocalStorageSupported() === false) { - return { - setItem() {}, - getItem() { - return []; - }, - }; - } - - return { - setItem(item) { - return window.localStorage.setItem(key, JSON.stringify(item)); - }, - getItem() { - const item = window.localStorage.getItem(key); - - return item ? JSON.parse(item) : []; - }, - }; -} - -export function createRecentSearchesStore({ key, limit }) { - const storage = createStorage(key); - let items = storage.getItem().slice(0, limit); - - return { - add(item) { - const isQueryAlreadySaved = items.findIndex( - (x) => x.objectID === item.objectID - ); - - if (isQueryAlreadySaved > -1) { - items.splice(isQueryAlreadySaved, 1); - } - - items.unshift(item); - items = items.slice(0, limit); - - storage.setItem(items); - }, - remove(item) { - items = items.filter((x) => x.objectID !== item.objectID); - - storage.setItem(items); - }, - getAll() { - return items; - }, - }; -} diff --git a/packages/autocomplete-plugin-recent-searches/src/createStore.ts b/packages/autocomplete-plugin-recent-searches/src/createStore.ts new file mode 100644 index 000000000..a0c24ebb5 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/createStore.ts @@ -0,0 +1,31 @@ +import { MaybePromise } from './types'; +import { RecentSearchesItem } from './types/RecentSearchesItem'; + +export type RecentSearchesStore = { + add(item: TItem): void; + remove(id: string): void; + getAll(query?: string): MaybePromise; +}; + +export type RecentSearchesStorage = { + onAdd(item: TItem): void; + onRemove(id: string): void; + getAll(query?: string): MaybePromise; +}; + +export function createStore( + storage: RecentSearchesStorage +): RecentSearchesStore { + return { + add(item) { + storage.onRemove(item.id); + storage.onAdd(item); + }, + remove(id) { + storage.onRemove(id); + }, + getAll(query) { + return storage.getAll(query); + }, + }; +} diff --git a/packages/autocomplete-plugin-recent-searches/src/getTemplates.ts b/packages/autocomplete-plugin-recent-searches/src/getTemplates.ts new file mode 100644 index 000000000..a84638dde --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/getTemplates.ts @@ -0,0 +1,36 @@ +import { recentIcon, resetIcon } from './icons'; + +export type GetTemplatesParams = { + onRemove(id: string): void; +}; + +export function getTemplates({ onRemove }: GetTemplatesParams) { + return { + item({ item, root }) { + const content = document.createElement('div'); + content.className = 'aa-ItemContent'; + const icon = document.createElement('div'); + icon.className = 'aa-ItemSourceIcon'; + icon.innerHTML = recentIcon; + const title = document.createElement('div'); + title.className = 'aa-ItemTitle'; + title.innerText = item.query; + content.appendChild(icon); + content.appendChild(title); + + const removeButton = document.createElement('button'); + removeButton.className = 'aa-ItemActionButton'; + removeButton.type = 'button'; + removeButton.innerHTML = resetIcon; + removeButton.title = 'Remove'; + + root.appendChild(content); + root.appendChild(removeButton); + + removeButton.addEventListener('click', (event) => { + event.stopPropagation(); + onRemove(item.id); + }); + }, + }; +} diff --git a/packages/autocomplete-plugin-recent-searches/src/icons/index.ts b/packages/autocomplete-plugin-recent-searches/src/icons/index.ts new file mode 100644 index 000000000..114894a05 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/icons/index.ts @@ -0,0 +1,2 @@ +export * from './recentIcon'; +export * from './resetIcon'; diff --git a/packages/autocomplete-plugin-recent-searches/src/icons/recentIcon.ts b/packages/autocomplete-plugin-recent-searches/src/icons/recentIcon.ts new file mode 100644 index 000000000..cf9ad96a9 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/icons/recentIcon.ts @@ -0,0 +1,6 @@ +export const recentIcon = ` + + + + +`; diff --git a/packages/autocomplete-plugin-recent-searches/src/resetIcon.ts b/packages/autocomplete-plugin-recent-searches/src/icons/resetIcon.ts similarity index 66% rename from packages/autocomplete-plugin-recent-searches/src/resetIcon.ts rename to packages/autocomplete-plugin-recent-searches/src/icons/resetIcon.ts index 1dd6aa7e6..aa82f9368 100644 --- a/packages/autocomplete-plugin-recent-searches/src/resetIcon.ts +++ b/packages/autocomplete-plugin-recent-searches/src/icons/resetIcon.ts @@ -1,5 +1,5 @@ -// copied from autocomplete-js -export const resetIcon = ` +export const resetIcon = ` + stroke-linecap="round" stroke-linejoin="round" > -`; + +`; diff --git a/packages/autocomplete-plugin-recent-searches/src/index.ts b/packages/autocomplete-plugin-recent-searches/src/index.ts index 072150b08..c0ac32530 100644 --- a/packages/autocomplete-plugin-recent-searches/src/index.ts +++ b/packages/autocomplete-plugin-recent-searches/src/index.ts @@ -1 +1,3 @@ -export { createRecentSearchesPlugin } from './createRecentSearchesPlugin'; +export * from './createLocalStorageRecentSearchesPlugin'; +export * from './createRecentSearchesPlugin'; +export * from './getTemplates'; diff --git a/packages/autocomplete-plugin-recent-searches/src/recentIcon.ts b/packages/autocomplete-plugin-recent-searches/src/recentIcon.ts deleted file mode 100644 index a7b27f4f6..000000000 --- a/packages/autocomplete-plugin-recent-searches/src/recentIcon.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const recentIcon = ` - - -`; diff --git a/packages/autocomplete-plugin-recent-searches/src/style.css b/packages/autocomplete-plugin-recent-searches/src/style.css deleted file mode 100644 index 5df365480..000000000 --- a/packages/autocomplete-plugin-recent-searches/src/style.css +++ /dev/null @@ -1,21 +0,0 @@ -.aa-RecentSearchesItem { - display: flex; - flex-grow: 1; - justify-content: space-between; -} - -.aa-RecentSearchesItem .leftItems { - display: flex; -} - -.aa-RecentSearchesItem .removeButton { - border: 0; - background-color: transparent; - cursor: pointer; - opacity: 0.8; -} - -.aa-RecentSearchesItem .removeButton:hover { - opacity: 1; - color: #000; -} diff --git a/packages/autocomplete-plugin-recent-searches/src/types/MaybePromise.ts b/packages/autocomplete-plugin-recent-searches/src/types/MaybePromise.ts new file mode 100644 index 000000000..3b8f1788e --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/types/MaybePromise.ts @@ -0,0 +1,2 @@ +// @TODO: reuse MaybePromise from autocomplete-core when we find a way to share the type +export type MaybePromise = Promise | TResolution; diff --git a/packages/autocomplete-plugin-recent-searches/src/types/RecentSearchesItem.ts b/packages/autocomplete-plugin-recent-searches/src/types/RecentSearchesItem.ts new file mode 100644 index 000000000..76aa96ed7 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/types/RecentSearchesItem.ts @@ -0,0 +1,4 @@ +export type RecentSearchesItem = { + id: string; + query: string; +}; diff --git a/packages/autocomplete-plugin-recent-searches/src/types/index.ts b/packages/autocomplete-plugin-recent-searches/src/types/index.ts new file mode 100644 index 000000000..98c53c3cf --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './MaybePromise'; +export * from './RecentSearchesItem'; diff --git a/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/constants.ts b/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/constants.ts new file mode 100644 index 000000000..dea5dec49 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/constants.ts @@ -0,0 +1,3 @@ +export const LOCAL_STORAGE_KEY = 'AUTOCOMPLETE_RECENT_SEARCHES'; +export const LOCAL_STORAGE_KEY_TEST = + '__AUTOCOMPLETE_RECENT_SEARCHES_PLUGIN_TEST_KEY__'; diff --git a/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/createLocalStorage.ts b/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/createLocalStorage.ts new file mode 100644 index 000000000..11e3ce18d --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/createLocalStorage.ts @@ -0,0 +1,29 @@ +import { CreateRecentSearchesLocalStorageOptions } from '../../createLocalStorageRecentSearchesPlugin'; +import { RecentSearchesStorage } from '../../createStore'; +import { RecentSearchesItem } from '../../types/RecentSearchesItem'; + +import { getLocalStorage } from './getLocalStorage'; + +export type CreateLocalStorageProps< + TItem extends RecentSearchesItem +> = Required>; + +export function createLocalStorage({ + key, + limit, + search, +}: CreateLocalStorageProps): RecentSearchesStorage { + const storage = getLocalStorage({ key }); + + return { + getAll(query = '') { + return search({ query, items: storage.getItem(), limit }).slice(0, limit); + }, + onAdd(item) { + storage.setItem([item, ...storage.getItem()]); + }, + onRemove(id) { + storage.setItem(storage.getItem().filter((x) => x.id !== id)); + }, + }; +} diff --git a/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/getLocalStorage.ts b/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/getLocalStorage.ts new file mode 100644 index 000000000..8554349d5 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/getLocalStorage.ts @@ -0,0 +1,27 @@ +import { isLocalStorageSupported } from './isLocalStorageSupported'; + +type LocalStorageProps = { + key: string; +}; + +export function getLocalStorage({ key }: LocalStorageProps) { + if (!isLocalStorageSupported()) { + return { + setItem() {}, + getItem() { + return []; + }, + }; + } + + return { + setItem(items: TItem[]) { + return window.localStorage.setItem(key, JSON.stringify(items)); + }, + getItem(): TItem[] { + const items = window.localStorage.getItem(key); + + return items ? (JSON.parse(items) as TItem[]) : []; + }, + }; +} diff --git a/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/index.ts b/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/index.ts new file mode 100644 index 000000000..014fefd8b --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/index.ts @@ -0,0 +1,4 @@ +export * from './constants'; +export * from './getLocalStorage'; +export * from './createLocalStorage'; +export * from './search'; diff --git a/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/isLocalStorageSupported.ts b/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/isLocalStorageSupported.ts new file mode 100644 index 000000000..6c73f6633 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/isLocalStorageSupported.ts @@ -0,0 +1,12 @@ +import { LOCAL_STORAGE_KEY_TEST } from './constants'; + +export function isLocalStorageSupported() { + try { + localStorage.setItem(LOCAL_STORAGE_KEY_TEST, ''); + localStorage.removeItem(LOCAL_STORAGE_KEY_TEST); + + return true; + } catch (error) { + return false; + } +} diff --git a/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/search.ts b/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/search.ts new file mode 100644 index 000000000..0c869da49 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/usecases/localStorage/search.ts @@ -0,0 +1,21 @@ +import { RecentSearchesItem } from '../../types'; + +export type SearchParams = { + query: string; + items: TItem[]; + limit: number; +}; + +export function search({ + query, + items, + limit, +}: SearchParams) { + if (!query) { + return items.slice(0, limit); + } + + return items + .filter((item) => item.query.toLowerCase().startsWith(query.toLowerCase())) + .slice(0, limit); +} diff --git a/packages/autocomplete-plugin-recent-searches/style/index.js b/packages/autocomplete-plugin-recent-searches/style/index.js deleted file mode 100644 index 722a95168..000000000 --- a/packages/autocomplete-plugin-recent-searches/style/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from '../dist/esm/style.css'; diff --git a/packages/autocomplete-preset-algolia/package.json b/packages/autocomplete-preset-algolia/package.json index 7f4e06bdb..d0c0fc52c 100644 --- a/packages/autocomplete-preset-algolia/package.json +++ b/packages/autocomplete-preset-algolia/package.json @@ -3,6 +3,16 @@ "description": "Presets for building autocomplete experiences with Algolia.", "version": "1.0.0-alpha.34", "license": "MIT", + "homepage": "https://github.com/algolia/autocomplete.js", + "repository": "algolia/autocomplete.js", + "author": { + "name": "Algolia, Inc.", + "url": "https://www.algolia.com" + }, + "sideEffects": false, + "files": [ + "dist/" + ], "source": "src/index.ts", "types": "dist/esm/index.d.ts", "module": "dist/esm/index.js", @@ -20,16 +30,6 @@ "prepare": "yarn build:esm && yarn build:types", "watch": "watch \"yarn on:change\" --ignoreDirectoryPattern \"/dist/\"" }, - "homepage": "https://github.com/algolia/autocomplete.js", - "repository": "algolia/autocomplete.js", - "author": { - "name": "Algolia, Inc.", - "url": "https://www.algolia.com" - }, - "sideEffects": false, - "files": [ - "dist/" - ], "dependencies": { "@algolia/client-search": "4.5.1", "algoliasearch": "4.5.1"