Skip to content
Open
58 changes: 43 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ npm i -D svelte-typeahead

Pass an array of objects to the `data` prop. Use the `extractor` to specify the key value to search on.

<!-- prettier-ignore-start -->
```svelte
<script>
import Typeahead from "svelte-typeahead";
Expand Down Expand Up @@ -83,9 +82,32 @@ Use a slot to render custom results.
```
<!-- prettier-ignore-end -->

### Selected Items

The `selection` can hold a function to return the items which should get the `selected` class in the results.

Example for switching items on select:
<!-- prettier-ignore-start -->
```svelte
<script>

function handleSelect(e) {
let i = e.detail.originalIndex;
data[i].selected = !data[i].selected;
}

</script>

<Typeahead {data} extract={(item) => item.state} selection={(item) => item.selected} on:select="{handleSelect} />
```
<!-- prettier-ignore-end -->

*Hint: Required items should match `selection` and `disabled` to be shown as selected and prevent them from unselection. Further styling may be needed.*
### Disable and Filter Items

Use the `filter` to filter Items out and `disable` to disable them in the result set.
- Filtered items are not part of the results at all.
- Disabled itesm receive the class `disbaled` and will not fire an `on:select` event.

Example for disabling and filtering items by their title length:

Expand All @@ -100,18 +122,19 @@ Example for disabling and filtering items by their title length:
```
<!-- prettier-ignore-end -->


Example for disabling items after selecting them:

<!-- prettier-ignore-start -->
```svelte
<script>
function handleSelect(e) {
let i = e.detail.originalIndex;
data[i].selected = true;
data[i].disabled = true;
}
</script>

<Typeahead {data} extract={(item) => item.state} disable={(item) => item.selected} on:select="{handleSelect}" />
<Typeahead {data} extract={(item) => item.state} disable={(item) => item.disabled} on:select="{handleSelect}" />
```
<!-- prettier-ignore-end -->

Expand All @@ -129,18 +152,19 @@ Set `focusAfterSelect` to `true` to re-focus the search input after selecting a

### Props

| Prop name | Value | Description |
| :--------------- | :-------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------- |
| value | `string` (default: `""`) | Input search value |
| data | `T[]` (default: `[]`) | Items to search |
| extract | `(T) => T` | Target an item key if `data` is an object array |
| disable | `(T) => T` | Pass in a function to disable items. They will show up in the results list, but wont be selectable. |
| filter | `(T) => T` | Pass in a function to filter items. Thei will be hidden and do not show up at all in the results list. |
| autoselect | `boolean` (default: `true`) | Automatically select the first (top) result |
| inputAfterSelect | `"update" or "clear" or "keep"`(default:`"update"`) | Set to `"clear"` to clear the `value` after selecting a result. Set to `"keep"` keep the search field unchanged after a selection. |
| results | `FuzzyResult[]` (default: `[]`) | Raw fuzzy results from the [fuzzy](https://github.com/mattyork/fuzzy) module |
| focusAfterSelect | `boolean` (default: `false`) | Set to `true` to re-focus the input after selecting a result. |
| `...$$restProps` | (forwarded to `Search` component) | All other props are forwarded to the input element. |
| Prop name | Value | Description |
| :--------------- | :-------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------- |
| value | `string` (default: `""`) | Input search value |
| data | `T[]` (default: `[]`) | Items to search |
| extract | `(T) => T` | Target an item key if `data` is an object array |
| selection | `(T) => T` | Pass in a function to select items. They will reveice the class `selected`. |
| disable | `(T) => T` | Pass in a function to disable items. They will show up in the results list and receive the class `disabled`, but wont be selectable. |
| filter | `(T) => T` | Pass in a function to filter items. Thei will be hidden and do not show up at all in the results list. |
| autoselect | `boolean` (default: `true`) | Automatically select the first (top) result |
| inputAfterSelect | `"update" or "clear" or "keep"`(default:`"update"`) | Set to `"clear"` to clear the `value` after selecting a result. Set to `"keep"` keep the search field unchanged after a selection. |
| results | `FuzzyResult[]` (default: `[]`) | Raw fuzzy results from the [fuzzy](https://github.com/mattyork/fuzzy) module |
| focusAfterSelect | `boolean` (default: `false`) | Set to `true` to re-focus the input after selecting a result. |
| `...$$restProps` | (forwarded to `Search` component) | All other props are forwarded to the input element. |

### Dispatched events

Expand Down Expand Up @@ -207,6 +231,10 @@ module.exports = {

Svelte version 3.31 or greater is required if using TypeScript.

## Internet Explorer

To make this component compatible with IE11 you'll need to polyfill `findIndex`.

## Changelog

[Changelog](CHANGELOG.md)
Expand Down
37 changes: 25 additions & 12 deletions src/Typeahead.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

/** @type {(item: Item) => Item} */
export let extract = (item) => item;

/** @type {(item: Item) => Item} */
export let selection = (item) => false;

/** @type {(item: Item) => Item} */
export let disable = (item) => false;
Expand Down Expand Up @@ -51,7 +54,7 @@

afterUpdate(() => {
if (prevResults !== resultsId && autoselect) {
selectedIndex = 0;
selectedIndex = results.findIndex(result => !result.disabled);
}

if (prevResults !== resultsId) {
Expand Down Expand Up @@ -89,7 +92,11 @@
.filter(value, data, options)
.filter(({ score }) => score > 0)
.filter((result) => !filter(result.original))
.map((result) => ({ ...result, disabled: disable(result.original) }));
.map((result)=> ({
...result,
disabled: disable(result.original),
selected: selection(result.original)
}));
$: resultsId = results.map((result) => extract(result.original)).join("");
</script>

Expand Down Expand Up @@ -139,16 +146,17 @@
break;
case 'ArrowDown':
e.preventDefault();
selectedIndex += 1;
if (selectedIndex === results.length) {
selectedIndex = 0;
for (selectedIndex++;(selectedIndex in results && results[selectedIndex].disabled); selectedIndex++);
if(!(selectedIndex in results) || (selectedIndex in results && results[selectedIndex].disabled)) {
selectedIndex = results.findIndex(result => !result.disabled);
}
break;
case 'ArrowUp':
e.preventDefault();
selectedIndex -= 1;
if (selectedIndex < 0) {
selectedIndex = results.length - 1;
for (selectedIndex--;(selectedIndex in results && results[selectedIndex].disabled); selectedIndex--);
if(!(selectedIndex in results) || (selectedIndex in results && results[selectedIndex].disabled)) {
let reverseselectedIndex = results.slice().reverse().findIndex(result => !result.disabled) + 1;
selectedIndex = (reverseselectedIndex == -1) ? -1 : (results.length - reverseselectedIndex);
}
break;
case 'Escape':
Expand All @@ -171,7 +179,8 @@
<li
role="option"
id="{id}-result"
class:selected={selectedIndex === i}
class:active={selectedIndex === i}
class:selected={result.selected}
class:disabled={result.disabled}
aria-selected={selectedIndex === i}
on:click={() => {
Expand Down Expand Up @@ -228,18 +237,22 @@
background-color: #cacaca;
}

.active {
background-color: #d8e9f3;
}

.disabled {
opacity: 0.4;
cursor: not-allowed;
}

:global([data-svelte-search] label) {
[data-svelte-typeahead] :global([data-svelte-search] label) {
margin-bottom: 0.25rem;
display: inline-flex;
font-size: 0.875rem;
}

:global([data-svelte-search] input) {
[data-svelte-typeahead] :global([data-svelte-search] input) {
width: 100%;
padding: 0.5rem 0.75rem;
background: none;
Expand All @@ -249,7 +262,7 @@
border: 1px solid #e5e5e5;
}

:global([data-svelte-search] input:focus) {
[data-svelte-typeahead] :global([data-svelte-search] input:focus) {
outline-color: #0f62fe;
outline-offset: 2px;
outline-width: 1px;
Expand Down
5 changes: 5 additions & 0 deletions types/Typeahead.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export interface TypeaheadProps extends SearchProps {
* @default (item) => item
*/
extract?: (item: Item) => Item;

/**
* @default (item) => item
*/
selection?: (item: Item) => Item;

/**
* @default (item) => false
Expand Down