Files
windmill/frontend/src/lib/components/apps/components/inputs/AppMultiSelectV2.svelte
T
2024-11-07 11:10:07 +01:00

257 lines
7.4 KiB
Svelte

<script lang="ts">
import { getContext } from 'svelte'
import { initConfig, initOutput } from '../../editor/appUtils'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { initCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
// @ts-ignore
import Portal from '$lib/components/Portal.svelte'
import { createFloatingActions } from 'svelte-floating-ui'
import { extractCustomProperties } from '$lib/utils'
import { tick } from 'svelte'
import { offset, flip, shift } from 'svelte-floating-ui/dom'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
import MultiSelect from '$lib/components/multiselect/MultiSelect.svelte'
import { isObjectOptionArray, isStringArray, type ObjectOption } from '../../../multiselect/types'
export let id: string
export let configuration: RichConfigurations
export let customCss: ComponentCustomCSS<'multiselectcomponent'> | undefined = undefined
export let render: boolean
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
const [floatingRef, floatingContent] = createFloatingActions({
strategy: 'absolute',
middleware: [offset(5), flip(), shift()]
})
const { app, worldStore, selectedComponent, componentControl } =
getContext<AppViewerContext>('AppViewerContext')
let items: ObjectOption[] = []
const resolvedConfig = initConfig(
components['multiselectcomponentv2'].initialData.configuration,
configuration
)
$: handleItems(resolvedConfig.items)
function handleItems(resolvedConfigItems) {
if (!resolvedConfigItems) {
return
}
items = []
value = []
if (isObjectOptionArray(resolvedConfigItems)) {
items = parseLabeledItems(resolvedConfigItems)
} else if (isStringArray(resolvedConfigItems)) {
items = parseStringItems(resolvedConfigItems)
}
}
const outputs = initOutput($worldStore, id, {
result: isObjectOptionArray(items)
? ([] as ObjectOption[])
: isStringArray(items)
? ([] as string[])
: ([] as unknown[])
})
let value: ObjectOption[] | undefined = [...new Set(outputs?.result.peak())] as ObjectOption[]
$componentControl[id] = {
setValue(nvalue: ObjectOption[]) {
if (isObjectOptionArray(nvalue)) {
value = [...new Set(nvalue)] as ObjectOption[]
outputs?.result.set([...(value ?? [])])
}
}
}
function parseLabeledItems(resolvedConfigItems: ObjectOption[]) {
return resolvedConfigItems?.map((item: ObjectOption) => {
if (!item || typeof item !== 'object') {
console.error(
'When labeled, MultiSelect component items should be an array of { label: string, value: string }.'
)
return {
label: 'not object',
value: 'not object'
}
}
return {
label: item?.label ?? 'undefined',
value:
typeof item?.value === 'object' ? JSON.stringify(item.value) : item?.value ?? 'undefined'
}
})
}
function parseStringItems(resolvedConfigItems: string[]): ObjectOption[] {
return resolvedConfigItems?.map((option: string) => {
if (option === null || option === undefined || typeof option !== 'string') {
console.error(
'When not labeled, MultiSelect component items should be an array of strings.'
)
return { label: 'not string', value: 'not string' }
}
if (option === '') {
return { label: 'empty string', value: 'empty string' }
}
return { label: option, value: option }
})
}
$: resolvedConfig.defaultItems && handleDefaultItems()
// todo
function handleDefaultItems() {
let nvalue: typeof items
if (!Array.isArray(resolvedConfig.defaultItems)) {
nvalue = []
outputs?.result.set([])
return
}
let rawNvalue = new Set(
resolvedConfig.defaultItems?.filter((v) => typeof v === 'string' || typeof v === 'number')
)
nvalue = items?.filter((item) => rawNvalue.has(item.label)) as ObjectOption[]
value = [...new Set(nvalue)]
outputs?.result.set([...(value ?? [])])
}
let css = initCss($app.css?.multiselectcomponent, customCss)
function setOuterDivStyle(outerDiv: HTMLDivElement, portalRef: HTMLDivElement, style: string) {
outerDiv.setAttribute('style', style)
// find ul in portalRef and set style
const ul = portalRef.querySelector('ul')
ul?.setAttribute('style', extractCustomProperties(style))
}
$: outerDiv &&
portalRef &&
css?.multiselect?.style &&
setOuterDivStyle(outerDiv, portalRef, css?.multiselect?.style)
let outerDiv: HTMLDivElement | undefined = undefined
let portalRef: HTMLDivElement | undefined = undefined
function moveOptionsToPortal() {
// Find ul element with class 'options' within the outerDiv
const ul = outerDiv?.querySelector('.options')
if (ul) {
// Move the ul element to the portal
portalRef?.appendChild(ul)
}
}
$: if (render && portalRef && outerDiv && items?.length > 0) {
tick().then(() => {
moveOptionsToPortal()
})
}
let w = 0
let open: boolean = false
</script>
{#each Object.keys(components['multiselectcomponent'].initialData.configuration) as key (key)}
<ResolveConfig
{id}
{key}
bind:resolvedConfig={resolvedConfig[key]}
configuration={configuration[key]}
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.multiselectcomponent}
/>
{/each}
<InitializeComponent {id} />
<AlignWrapper {render} hFull {verticalAlignment}>
<div
class="w-full app-editor-input"
on:pointerdown={(e) => {
$selectedComponent = [id]
if (!e.shiftKey) {
e.stopPropagation()
}
selectedComponent.set([id])
}}
use:floatingRef
bind:clientWidth={w}
>
{#if !value || Array.isArray(value)}
<MultiSelect
bind:outerDiv
outerDivClass={`${resolvedConfig.allowOverflow ? '' : 'h-full'}`}
ulSelectedClass={`${resolvedConfig.allowOverflow ? '' : 'overflow-auto max-h-full'} `}
--sms-border={'none'}
--sms-min-height={'32px'}
--sms-focus-border={'none'}
bind:selected={value}
options={items}
placeholder={resolvedConfig.placeholder}
allowUserOptions={resolvedConfig.create}
on:change={(event) => {
if (event?.detail?.type === 'removeAll') {
outputs?.result.set([])
} else {
outputs?.result.set([...(value ?? [])])
}
}}
on:open={() => {
$selectedComponent = [id]
open = true
}}
on:close={() => {
open = false
}}
let:option
>
<!-- needed because portal doesn't work for mouseup event en mobile -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="w-full"
on:mouseup|stopPropagation
on:pointerdown|stopPropagation={(e) => {
let newe = new MouseEvent('mouseup')
e.target?.['parentElement']?.dispatchEvent(newe)
}}
>
{isObjectOptionArray(value) && isObjectOptionArray(items) ? option.label : option}
</div>
</MultiSelect>
<Portal name="app-multiselect-v2">
<div use:floatingContent class="z5000" hidden={!open}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
bind:this={portalRef}
class="multiselect"
style={`min-width: ${w}px;`}
on:click|stopPropagation
/>
</div>
</Portal>
{:else}
Value {value} is not an array
{/if}
</div>
</AlignWrapper>