mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-04 16:03:06 +00:00
257 lines
7.4 KiB
Svelte
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>
|