Skip to content

Commit

Permalink
feat(Select): added multi select ability
Browse files Browse the repository at this point in the history
  • Loading branch information
N00nDay committed Mar 7, 2023
1 parent 9a857da commit 314f30e
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 29 deletions.
37 changes: 30 additions & 7 deletions src/lib/components/select/Option.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,35 @@
import { forwardEventsBuilder, useActions, type ActionArray } from '../../actions';
export let use: ActionArray = [];
import { exclude } from '../../utils/exclude';
import type { SelectOption } from '$lib/types/select-option';
const forwardEvents = forwardEventsBuilder(get_current_component());
export let option: string;
export let option: SelectOption;
const value: Writable<string> = getContext('select-value');
const handleSelect: (option: string) => void = getContext('select-handleSelect');
const value: Writable<SelectOption | SelectOption[] | undefined> = getContext('select-value');
const optionLabel: string = getContext('select-option-label');
const optionValue: string = getContext('select-option-value');
const handleSelect: (option: SelectOption) => void = getContext('select-handleSelect');
const multiple: boolean = getContext('select-multiple');
let optionIsSelected = false;
$: {
if ($value && multiple && Array.isArray($value)) {
const isSelected = $value.find((v) => v[optionValue] === option[optionValue]);
if (isSelected) {
optionIsSelected = true;
} else {
optionIsSelected = false;
}
} else if ($value && !Array.isArray($value)) {
if ($value[optionValue] === option[optionValue]) {
optionIsSelected = true;
} else {
optionIsSelected = false;
}
}
}
const defaultClass =
'group text-light-content dark:text-dark-content cursor-pointer select-none p-0.5 w-full';
Expand All @@ -25,18 +48,18 @@
<li
class={finalClass}
role="option"
aria-selected={option === $value}
aria-selected={optionIsSelected}
use:useActions={use}
use:forwardEvents
{...exclude($$props, ['use', 'class'])}
>
<button aria-label="select option" on:click={() => handleSelect(option)} class="w-full text-left">
<div class="relative py-1.5 pl-2.5 pr-7 w-full rounded-md overflow-hidden">
<span class="font-normal block truncate" class:font-semibold={option === $value}>
{option}
<span class="font-normal block truncate" class:font-semibold={optionIsSelected}>
{option[optionLabel]}
</span>

{#if option === $value}
{#if optionIsSelected}
<span
transition:scale|local
class="text-primary absolute inset-y-0 right-0 flex items-center pr-1.5"
Expand Down
101 changes: 85 additions & 16 deletions src/lib/components/select/Select.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
<script lang="ts">
import { setContext } from 'svelte';
import { slide } from 'svelte/transition';
import { scale, slide } from 'svelte/transition';
import { clickOutside } from '../../actions';
import { Icon } from '../../';
import { unfold_more_horizontal, error as errorIcon } from '../../icons';
import { writable, type Writable } from 'svelte/store';
import type { SelectOption } from '../../types/select-option';
import { flip } from 'svelte/animate';
import Badge from '../badge';
export let name: string;
export let error: string | undefined = undefined;
export let placeholder: string | undefined = undefined;
export let value: string | undefined = undefined;
export let value: SelectOption | SelectOption[] | undefined = undefined;
export let visible = false;
export let optionLabel = 'label';
export let optionValue = 'value';
export let multiple = false;
export let closeOnSelect = true;
let selectedValue = writable(value);
$: stringifyValues = multiple
? JSON.stringify(value)
: value && value instanceof Object && !Array.isArray(value)
? value[optionValue]
: '';
let selectedValue: Writable<SelectOption | SelectOption[] | undefined> = writable(value);
let currentError: Writable<string | undefined> = writable(error);
$: currentError.set(error);
Expand All @@ -26,27 +38,63 @@
visible = false;
}
function handleSelect(option: string) {
input.value = option;
value = option;
$selectedValue = option;
toggleVisible();
function handleSelect(option: SelectOption) {
if (multiple) {
const tempSelectedValues = ($selectedValue as SelectOption[]) || [];
const selectedIndex = tempSelectedValues.findIndex((sv) => sv.value === option.value);
if (selectedIndex !== -1) {
tempSelectedValues.splice(selectedIndex, 1);
$selectedValue = tempSelectedValues;
} else {
tempSelectedValues.push(option);
$selectedValue = tempSelectedValues;
}
input.value = JSON.stringify($selectedValue);
value = $selectedValue;
} else {
if (value && !Array.isArray(value) && value[optionValue] === option[optionValue]) {
input.value = '';
value = undefined;
$selectedValue = undefined;
} else {
input.value = option[optionValue];
value = option;
$selectedValue = option;
}
}
if (closeOnSelect) {
console.log('closed FIRED');
toggleVisible();
}
}
function handleRemoveOption(e: Event, index: number) {
e.stopPropagation();
e.preventDefault();
const tempSelectedValues = $selectedValue as SelectOption[];
tempSelectedValues.splice(index, 1);
$selectedValue = tempSelectedValues;
input.value = JSON.stringify($selectedValue);
value = $selectedValue;
}
setContext('select-error', currentError);
setContext('select-name', name);
setContext('select-value', selectedValue);
setContext('select-handleSelect', handleSelect);
setContext('select-option-label', optionLabel);
setContext('select-option-value', optionValue);
setContext('select-multiple', multiple);
</script>

<div class={$$props.class} style={$$props.style} use:clickOutside={handleClose}>
<slot name="label" />
<div class="mt-1 relative rounded-md h-[2.5rem]" class:text-danger={error}>
<div class="mt-1 relative rounded-md" class:text-danger={error}>
<button
aria-label="toggle select"
type="button"
on:click|stopPropagation|preventDefault={toggleVisible}
class="relative border h-[2.5rem] cursor-pointer pl-3 pr-10 py-2 min-h-[2.5rem] text-left focus:outline-none sm:text-sm block w-full outline-none ring-0 focus:ring-0 rounded-md bg-light-surface dark:bg-dark-surface"
class="relative border cursor-pointer pl-3 pr-10 py-2 min-h-[2.5rem] text-left focus:outline-none sm:text-sm block w-full outline-none ring-0 focus:ring-0 rounded-md bg-light-surface dark:bg-dark-surface"
class:border-red-400={error}
class:text-danger={error}
class:dark:text-danger={error}
Expand All @@ -59,18 +107,37 @@
class:pl-10={$$slots.leading}
>
<span
class="block truncate text-light-content dark:text-dark-content"
class="flex flex-row flex-wrap gap-2 truncate text-light-content dark:text-dark-content"
class:pl-1.5={$$slots.leading}
class:text-gray-500={placeholder && !value}
class:dark:text-gray-500={placeholder && !value}
class:text-gray-500={placeholder && (!value || value.length === 0)}
class:dark:text-gray-500={placeholder && (!value || value.length === 0)}
>
{value ? value : placeholder ? placeholder : ''}
{#if multiple}
{#if value && value.length > 0 && Array.isArray(value)}
{#each value as item, index (item)}
<span animate:flip={{ duration: 250 }} in:scale|local>
<Badge>
{item[optionLabel] || item.value}
<Badge.Close slot="close" on:click={(e) => handleRemoveOption(e, index)} />
</Badge>
</span>
{/each}
{:else if placeholder}
{placeholder}
{/if}
{:else if !Array.isArray(value)}
{#if value && value[optionValue]}
{value[optionLabel]}
{:else if placeholder}
{placeholder}
{/if}
{/if}
</span>
<input
{name}
id={name}
bind:this={input}
bind:value
bind:value={stringifyValues}
class="h-0 w-0 hidden invisible"
readonly
autocomplete="off"
Expand All @@ -87,7 +154,9 @@
<Icon data={errorIcon} />
</span>
{:else}
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<span
class="absolute inset-y-0 right-0 flex items-start pt-[0.4rem] pr-2 pointer-events-none"
>
<Icon data={unfold_more_horizontal} />
</span>
{/if}
Expand Down
3 changes: 2 additions & 1 deletion src/lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import type { Action as LightboxAction } from './lightbox-action';
import type { TableColumn } from './table-column';
import type { TimelineItem } from './timeline-item';
import type TwSizes from './twSizes';
import type { SelectOption } from './select-option';

export { CarouselSlide, LightboxAction, TableColumn, TimelineItem, TwSizes };
export { CarouselSlide, LightboxAction, TableColumn, TimelineItem, TwSizes, SelectOption };
3 changes: 3 additions & 0 deletions src/lib/types/select-option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface SelectOption {
[key: string]: any;
}
65 changes: 62 additions & 3 deletions src/routes/select/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { Card, Col, Select } from '../../lib';
import {
example,
exampleMultiple,
props,
slots,
labelSlots,
Expand All @@ -11,12 +12,52 @@
} from './examples';
import { PropsTable, SlotsTable, CodeBlock } from '../../docs';
import { email } from '../../docs/icons';
import type { SelectOption } from '$lib/types';
const options = ['Option 1', 'Option 2', 'Option 3'];
const options = [
{
value: 'option_1',
label: 'Option 1'
},
{
value: 'option_2',
label: 'Option 2'
},
{
value: 'option_3',
label: 'Option 3'
}
];
const multipleOptions = [
{
value: 'option_1',
label: 'Option 1'
},
{
value: 'option_2',
label: 'Option 2'
},
{
value: 'option_3',
label: 'Option 3'
},
{
value: 'option_4',
label: 'Option 4'
},
{
value: 'option_5',
label: 'Option 5'
},
{
value: 'option_6',
label: 'Option 6'
}
];
let value: string | undefined;
let value: SelectOption | undefined;
let error: string | undefined = "You're doing it wrong!";
$: if (value && value.length > 0) {
$: if (value && value.value) {
error = undefined;
} else {
error = "You're doing it wrong!";
Expand Down Expand Up @@ -61,6 +102,24 @@
</Card>
</Col>

<Col class="col-24 md:col-12">
<Card bordered={false}>
<Card.Content slot="content" class="p-4">
<Select name="select-4" placeholder="Basic" multiple>
<Select.Options slot="options">
{#each multipleOptions as option}
<Select.Options.Option {option} />
{/each}
</Select.Options>
</Select>

<br />

<CodeBlock language="svelte" code={exampleMultiple} />
</Card.Content>
</Card>
</Col>

<Col class="col-24">
<PropsTable component="Select" {props} />
</Col>
Expand Down
51 changes: 50 additions & 1 deletion src/routes/select/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ export const props: Prop[] = [
{
id: '4',
prop: 'value',
type: 'string | undefined',
type: '<a href="/types#SelectOption" class="link">SelectOption</a> | undefined',
default: ''
},
{
id: '5',
prop: 'multiple',
type: 'boolean',
default: 'false'
},
{
id: '6',
prop: 'visible',
type: 'boolean',
default: 'false'
Expand Down Expand Up @@ -170,3 +176,46 @@ export const example = `
{/each}
</Select.Options>
</Select>`;

export const exampleMultiple = `
<script lang="ts">
import { Select } from 'stwui';
import { SelectOption } from 'stwui/types';
const email = "svg-path";
const options: SelectOption[] = [
{
value: 'option_1',
label: 'Option 1'
},
{
value: 'option_2',
label: 'Option 2'
},
{
value: 'option_3',
label: 'Option 3'
},
{
value: 'option_4',
label: 'Option 4'
},
{
value: 'option_5',
label: 'Option 5'
},
{
value: 'option_6',
label: 'Option 6'
}
];
</script>
<Select name="select-4" placeholder="Basic" multiple>
<Select.Options slot="options">
{#each multipleOptions as option}
<Select.Options.Option {option} />
{/each}
</Select.Options>
</Select>
`;
Loading

3 comments on commit 314f30e

@vercel
Copy link

@vercel vercel bot commented on 314f30e Mar 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

stwui – ./

stwui-n00nday.vercel.app
stwui-git-main-n00nday.vercel.app
stwui.vercel.app

@dominic-schmid
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good!

Did you forget to adjust the props / slots table or are you going to add it with more documentation later?

@N00nDay
Copy link
Owner Author

@N00nDay N00nDay commented on 314f30e Mar 7, 2023 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.