This repository has been archived by the owner on Jun 11, 2021. It is now read-only.
forked from algolia/autocomplete
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(autocomplete): introduce JavaScript API
- Loading branch information
1 parent
50b4879
commit fd9d2b7
Showing
11 changed files
with
422 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
{ | ||
"name": "@francoischalifour/autocomplete-js", | ||
"description": "Fast and fully-featured autocomplete JavaScript library.", | ||
"version": "1.0.0-alpha.27", | ||
"license": "MIT", | ||
"source": "src/index.ts", | ||
"types": "dist/esm/index.d.ts", | ||
"module": "dist/esm/index.js", | ||
"main": "dist/umd/index.js", | ||
"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", | ||
"scripts": { | ||
"build": "rm -rf ./dist && yarn build:umd && yarn build:esm && yarn build:types", | ||
"build:esm": "babel src --root-mode upward --extensions '.ts,.tsx' --out-dir dist/esm", | ||
"build:esm:watch": "yarn build:esm --watch", | ||
"build:umd": "rollup --config", | ||
"build:types": "tsc -p ./tsconfig.declaration.json --outDir ./dist/esm", | ||
"build:clean": "rm -rf ./dist", | ||
"on:change": "concurrently \"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": { | ||
"@francoischalifour/autocomplete-core": "^1.0.0-alpha.27", | ||
"@francoischalifour/autocomplete-preset-algolia": "^1.0.0-alpha.27" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { plugins } from '../../rollup.base.config'; | ||
|
||
import { name } from './package.json'; | ||
|
||
if (process.env.NODE_ENV === 'production' && !process.env.VERSION) { | ||
throw new Error( | ||
`You need to specify a valid semver environment variable 'VERSION' to run the build process (received: ${JSON.stringify( | ||
process.env.VERSION | ||
)}).` | ||
); | ||
} | ||
|
||
export default { | ||
input: 'src/index.ts', | ||
output: { | ||
file: 'dist/umd/index.js', | ||
format: 'umd', | ||
sourcemap: true, | ||
name, | ||
}, | ||
plugins, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
import { | ||
createAutocomplete, | ||
AutocompleteOptions as AutocompleteCoreOptions, | ||
AutocompleteSource as AutocompleteCoreSource, | ||
AutocompleteState, | ||
GetSourcesParams, | ||
} from '@francoischalifour/autocomplete-core'; | ||
|
||
import { getHTMLElement } from './getHTMLElement'; | ||
import { setProperties } from './setProperties'; | ||
|
||
function renderTemplate(template: string | void, root: HTMLElement) { | ||
if (typeof template === 'string') { | ||
root.innerHTML = template; | ||
} | ||
} | ||
|
||
function defaultRender({ root, sections }) { | ||
for (const section of sections) { | ||
root.appendChild(section); | ||
} | ||
} | ||
|
||
type Template<TParams> = (params: TParams) => string | void; | ||
|
||
type AutocompleteSource<TItem> = AutocompleteCoreSource<TItem> & { | ||
templates: { | ||
item: Template<{ | ||
root: HTMLElement; | ||
item: TItem; | ||
state: AutocompleteState<TItem>; | ||
}>; | ||
header?: Template<{ root: HTMLElement; state: AutocompleteState<TItem> }>; | ||
footer?: Template<{ root: HTMLElement; state: AutocompleteState<TItem> }>; | ||
}; | ||
}; | ||
|
||
type GetSources<TItem> = ( | ||
params: GetSourcesParams<TItem> | ||
) => Promise<Array<AutocompleteSource<TItem>>>; | ||
|
||
export interface AutocompleteOptions<TItem> | ||
extends AutocompleteCoreOptions<TItem> { | ||
container: string | HTMLElement; | ||
render(params: { root: HTMLElement; sections: HTMLElement[] }): void; | ||
getSources: GetSources<TItem>; | ||
} | ||
|
||
export function autocomplete<TItem>({ | ||
container, | ||
render: renderDropdown = defaultRender, | ||
...props | ||
}: AutocompleteOptions<TItem>) { | ||
const containerElement = getHTMLElement(container, props.environment); | ||
const inputWrapper = document.createElement('div'); | ||
const input = document.createElement('input'); | ||
const root = document.createElement('div'); | ||
const form = document.createElement('form'); | ||
const label = document.createElement('label'); | ||
const resetButton = document.createElement('button'); | ||
const dropdown = document.createElement('div'); | ||
|
||
const autocomplete = createAutocomplete({ | ||
onStateChange(options) { | ||
const { state } = options; | ||
render(state as any); | ||
|
||
if (props.onStateChange) { | ||
props.onStateChange(options); | ||
} | ||
}, | ||
...props, | ||
}); | ||
|
||
const environmentProps = autocomplete.getEnvironmentProps({ | ||
searchBoxElement: form, | ||
dropdownElement: dropdown, | ||
inputElement: input, | ||
}); | ||
|
||
setProperties(window, environmentProps); | ||
|
||
const rootProps = autocomplete.getRootProps(); | ||
setProperties(root, rootProps); | ||
|
||
const formProps = autocomplete.getFormProps({ inputElement: input }); | ||
setProperties(form, formProps); | ||
form.setAttribute('action', ''); | ||
form.setAttribute('role', 'search'); | ||
form.setAttribute('no-validate', ''); | ||
form.classList.add('algolia-autocomplete-form'); | ||
|
||
const labelProps = autocomplete.getLabelProps(); | ||
setProperties(label, labelProps); | ||
label.textContent = 'Search items'; | ||
|
||
inputWrapper.classList.add('autocomplete-input-wrapper'); | ||
|
||
const inputProps = autocomplete.getInputProps({ inputElement: input }); | ||
setProperties(input, inputProps); | ||
|
||
const completion = document.createElement('span'); | ||
completion.classList.add('autocomplete-completion'); | ||
|
||
resetButton.setAttribute('type', 'reset'); | ||
resetButton.textContent = 'x'; | ||
resetButton.addEventListener('click', formProps.onReset); | ||
|
||
const dropdownProps = autocomplete.getDropdownProps({}); | ||
setProperties(dropdown, dropdownProps); | ||
dropdown.classList.add('autocomplete-dropdown'); | ||
dropdown.setAttribute('hidden', ''); | ||
|
||
function render(state: AutocompleteState<TItem>) { | ||
input.value = state.query; | ||
|
||
if (props.showCompletion) { | ||
completion.textContent = state.completion; | ||
} | ||
|
||
dropdown.innerHTML = ''; | ||
|
||
if (state.isOpen) { | ||
dropdown.removeAttribute('hidden'); | ||
} else { | ||
dropdown.setAttribute('hidden', ''); | ||
return; | ||
} | ||
|
||
if (state.status === 'stalled') { | ||
dropdown.classList.add('autocomplete-dropdown--stalled'); | ||
} else { | ||
dropdown.classList.remove('autocomplete-dropdown--stalled'); | ||
} | ||
|
||
const sections = state.suggestions.map((suggestion) => { | ||
const items = suggestion.items; | ||
const source = suggestion.source as AutocompleteSource<TItem>; | ||
|
||
const section = document.createElement('section'); | ||
|
||
if (source.templates.header) { | ||
const header = document.createElement('header'); | ||
renderTemplate( | ||
source.templates.header({ root: header, state }), | ||
header | ||
); | ||
section.appendChild(header); | ||
} | ||
|
||
if (items.length > 0) { | ||
const menu = document.createElement('ul'); | ||
const menuProps = autocomplete.getMenuProps(); | ||
setProperties(menu, menuProps); | ||
|
||
const menuItems = items.map((item) => { | ||
const li = document.createElement('li'); | ||
const itemProps = autocomplete.getItemProps({ item, source }); | ||
setProperties(li, itemProps); | ||
|
||
renderTemplate(source.templates.item({ root: li, item, state }), li); | ||
|
||
return li; | ||
}); | ||
|
||
for (const menuItem of menuItems) { | ||
menu.appendChild(menuItem); | ||
} | ||
|
||
section.appendChild(menu); | ||
} | ||
|
||
if (source.templates.footer) { | ||
const footer = document.createElement('footer'); | ||
renderTemplate( | ||
source.templates.footer({ root: footer, state }), | ||
footer | ||
); | ||
section.appendChild(footer); | ||
} | ||
|
||
return section; | ||
}); | ||
|
||
renderDropdown({ root: dropdown, sections }); | ||
} | ||
|
||
form.appendChild(label); | ||
if (props.showCompletion) { | ||
inputWrapper.appendChild(completion); | ||
} | ||
inputWrapper.appendChild(input); | ||
inputWrapper.appendChild(resetButton); | ||
form.appendChild(inputWrapper); | ||
root.appendChild(form); | ||
root.appendChild(dropdown); | ||
containerElement.appendChild(root); | ||
|
||
return autocomplete; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { AutocompleteOptions } from '@francoischalifour/autocomplete-core'; | ||
|
||
export function getHTMLElement( | ||
value: string | HTMLElement, | ||
environment: AutocompleteOptions<any>['environment'] | ||
): HTMLElement { | ||
if (typeof value === 'string') { | ||
return environment.document.querySelector<HTMLElement>(value)!; | ||
} | ||
|
||
return value; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { | ||
parseHighlightedAttribute, | ||
parseReverseHighlightedAttribute, | ||
} from '@francoischalifour/autocomplete-preset-algolia'; | ||
|
||
type HighlightItemParams = { | ||
item: any; | ||
attribute: string; | ||
highlightPreTag?: string; | ||
highlightPostTag?: string; | ||
}; | ||
|
||
export function highlightItem({ | ||
item, | ||
attribute, | ||
highlightPreTag = '<mark>', | ||
highlightPostTag = '</mark>', | ||
}: HighlightItemParams) { | ||
return parseHighlightedAttribute({ | ||
hit: item, | ||
attribute, | ||
highlightPreTag, | ||
highlightPostTag, | ||
}).reduce((acc, current) => { | ||
return ( | ||
acc + | ||
(current.isHighlighted | ||
? current.value | ||
: `${highlightPreTag}${current.value}${highlightPostTag}`) | ||
); | ||
}, ''); | ||
} | ||
|
||
export function reverseHighlightItem({ | ||
item, | ||
attribute, | ||
highlightPreTag = '<mark>', | ||
highlightPostTag = '</mark>', | ||
}: HighlightItemParams) { | ||
return parseReverseHighlightedAttribute({ | ||
hit: item, | ||
attribute, | ||
highlightPreTag, | ||
highlightPostTag, | ||
}).reduce((acc, current) => { | ||
return ( | ||
acc + | ||
(current.isHighlighted | ||
? current.value | ||
: `${highlightPreTag}${current.value}${highlightPostTag}`) | ||
); | ||
}, ''); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export * from './autocomplete'; | ||
export * from './highlight'; | ||
export { | ||
getAlgoliaResults, | ||
getAlgoliaHits, | ||
} from '@francoischalifour/autocomplete-preset-algolia'; |
Oops, something went wrong.