diff --git a/README.md b/README.md index 1103238..929681a 100644 --- a/README.md +++ b/README.md @@ -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. - ```svelte + + item.state} selection={(item) => item.selected} on:select="{handleSelect} /> +``` + + +*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: @@ -100,6 +122,7 @@ Example for disabling and filtering items by their title length: ``` + Example for disabling items after selecting them: @@ -107,11 +130,11 @@ Example for disabling items after selecting them: - item.state} disable={(item) => item.selected} on:select="{handleSelect}" /> + item.state} disable={(item) => item.disabled} on:select="{handleSelect}" /> ``` @@ -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 @@ -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) diff --git a/src/Typeahead.svelte b/src/Typeahead.svelte index 0fbc7d7..f6c192e 100644 --- a/src/Typeahead.svelte +++ b/src/Typeahead.svelte @@ -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; @@ -51,7 +54,7 @@ afterUpdate(() => { if (prevResults !== resultsId && autoselect) { - selectedIndex = 0; + selectedIndex = results.findIndex(result => !result.disabled); } if (prevResults !== resultsId) { @@ -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(""); @@ -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': @@ -171,7 +179,8 @@
  • { @@ -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; @@ -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; diff --git a/types/Typeahead.d.ts b/types/Typeahead.d.ts index d1a65b7..6e66826 100644 --- a/types/Typeahead.d.ts +++ b/types/Typeahead.d.ts @@ -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