Skip to content

Commit

Permalink
feat: auto-open filter popover on add filter
Browse files Browse the repository at this point in the history
  • Loading branch information
paring-chan committed Oct 16, 2024
1 parent 08dbe2e commit 4772a97
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 42 deletions.
54 changes: 49 additions & 5 deletions src/lib/components/Popover.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
<script lang="ts">
import { createPopover, createSync, melt } from '@melt-ui/svelte'
import type { FloatingConfig } from '@melt-ui/svelte/internal/actions'
import type { Snippet } from 'svelte'
import { fly } from 'svelte/transition'
import { onMount, tick, type Snippet } from 'svelte'
import { fly, type FlyParams, type TransitionConfig } from 'svelte/transition'
interface Props {
placement?: Exclude<FloatingConfig, null>['placement']
open?: boolean
trigger: Snippet<[typeof trigger]>
children: Snippet<[{ close: () => void; open: boolean }]>
defaultOpen?: boolean
lockOnClose?: boolean
}
let { placement, open = $bindable(false), trigger: triggerSnippet, children }: Props = $props()
let {
placement,
open = $bindable(false),
trigger: triggerSnippet,
children,
defaultOpen = false,
lockOnClose
}: Props = $props()
const {
elements: { trigger, content },
Expand All @@ -20,7 +29,9 @@
positioning: {
placement
},
forceVisible: true
forceVisible: true,
defaultOpen
})
const sync = createSync(states)
Expand All @@ -42,12 +53,45 @@
el?.focus()
}
})
onMount(() => {
if (defaultOpen) {
tick().then(() => {
states.open.set(true)
})
}
})
const out = (el: HTMLElement, params: FlyParams): TransitionConfig => {
const actualTransition = fly(el, params)
if (!lockOnClose) return actualTransition
const startTop = el.style.top
const startLeft = el.style.left
return {
css: actualTransition.css,
delay: actualTransition.delay,
duration: actualTransition.duration,
easing: actualTransition.easing,
tick: () => {
el.style.top = startTop
el.style.left = startLeft
}
}
}
</script>

{@render triggerSnippet(trigger)}

{#if open}
<div bind:this={ref} use:melt={$content} transition:fly={{ y: 12 }}>
<div
bind:this={ref}
use:melt={$content}
in:fly={{ y: 12, duration: 400 }}
out:out={{ y: 12, duration: 400 }}
>
{@render children({ close, open })}
</div>
{/if}
1 change: 1 addition & 0 deletions src/lib/components/search/PopoverSelect.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
}
} else if (e.key === 'Enter') {
e.preventDefault()
if (!filteredItems.length) return
if (!selected) return
onSelect?.(selected.value)
}
Expand Down
104 changes: 67 additions & 37 deletions src/lib/components/search/SearchOptionsBar.svelte
Original file line number Diff line number Diff line change
@@ -1,28 +1,47 @@
<script lang="ts">
import type { SearchOptionScheme, SearchOptionsData } from '$lib/types'
import { getGlobalContext, translate, translateKey, Translation } from '$lib/index'
import { translateKey, Translation } from '$lib/index'
import Popover from '../Popover.svelte'
import AddFilterButton from './AddFilterButton.svelte'
import PopoverSelect from './PopoverSelect.svelte'
import SearchOptionChip from './SearchOptionChip.svelte'
import PopoverContentPanel from '../PopoverContentPanel.svelte'
import FilterEditPanel from './FilterEditPanel.svelte'
import { circOut } from 'svelte/easing'
import { flip } from 'svelte/animate'
import { tick } from 'svelte'
import { fade, fly, slide } from 'svelte/transition'
interface Props {
scheme: SearchOptionScheme
data: SearchOptionsData
}
let { scheme, data = $bindable() }: Props = $props()
let defaultOpen = $state<string | null>(null)
const genKey = (key: string) => `${key}-${Date.now()}`
const addFilter = (key: string) => {
data.filter.push({
const id = genKey(key)
const newData = { ...data }
newData.filter = [...data.filter]
newData.filter.push({
key,
value: scheme.filter[key].default
value: scheme.filter[key].default,
id
})
data = newData
defaultOpen = id
}
const { language } = getGlobalContext()
$effect(() => {
for (const f of data.filter) {
if (!f.id) f.id = genKey(f.key)
}
})
</script>

<div class="search-options-bar">
Expand Down Expand Up @@ -55,50 +74,61 @@
<div class="divider"></div>
{/if}

{#each data.filter as filter, i}
{#each data.filter as filter, i (filter.id)}
{@const filterScheme = scheme.filter[filter.key]}
<Popover>
{#snippet trigger(el)}
<SearchOptionChip
icon={filterScheme.icon}
objectiveKey={filterScheme.name}
meltElement={el}
hasValue={!!filter.value}
>
{#if filterScheme.type === 'string'}
{filter.value}
{/if}
</SearchOptionChip>
{/snippet}
<div animate:flip={{ duration: 400 }}>
<Popover defaultOpen={defaultOpen === filter.id}>
{#snippet trigger(el)}
<div in:fade={{ duration: 400 }} out:fade={{ duration: 200 }}>
<SearchOptionChip
icon={filterScheme.icon}
objectiveKey={filterScheme.name}
meltElement={el}
hasValue={!!filter.value}
>
{#if filterScheme.type === 'string'}
{filter.value}
{/if}
</SearchOptionChip>
</div>
{/snippet}

{#snippet children({ close })}
<PopoverContentPanel>
<FilterEditPanel
onRemove={() => {
data.filter.splice(i, 1)
}}
{close}
scheme={filterScheme}
value={filter.value}
onSave={(value) => {
filter.value = value
close()
}}
/>
</PopoverContentPanel>
{/snippet}
</Popover>
{#snippet children({ close })}
<PopoverContentPanel>
<FilterEditPanel
onRemove={() => {
close()

tick().then(() => {
data.filter.splice(i, 1)
})
}}
{close}
scheme={filterScheme}
value={filter.value}
onSave={(value) => {
filter.value = value
close()
}}
/>
</PopoverContentPanel>
{/snippet}
</Popover>
</div>
{/each}
<Popover>

<Popover lockOnClose>
{#snippet trigger(el)}
<AddFilterButton meltElement={el} />
{/snippet}

{#snippet children({ close })}
<PopoverSelect
onSelect={(v) => {
addFilter(v)
close()
tick().then(() => {
addFilter(v)
})
}}
placeholder={(label, key, lang) =>
translateKey(lang, 'ui-search:add-filter-placeholder', { label: label || '...' })}
Expand Down
1 change: 1 addition & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export type SearchOptionScheme = {
)

export interface SearchFilter {
id: string
key: string
value: unknown
}
Expand Down

0 comments on commit 4772a97

Please sign in to comment.