Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add selection #13

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
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