mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 16:03:47 +00:00
229 lines
6.4 KiB
Svelte
229 lines
6.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 type { ObjectOption } from '../../../multiselect/types'
|
|
import { parseConfigOptions } from './utils'
|
|
import { deepEqual } from 'fast-equals'
|
|
|
|
export let configuration: RichConfigurations
|
|
export let customCss: ComponentCustomCSS<'multiselectcomponent'> | undefined = undefined
|
|
export let id: string
|
|
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')
|
|
|
|
const resolvedConfig = initConfig(
|
|
components['multiselectcomponentv2'].initialData.configuration,
|
|
configuration
|
|
)
|
|
const outputs = initOutput($worldStore, id, { result: [] as any[] })
|
|
let options: ObjectOption[] = []
|
|
let selectedOptions: ObjectOption[] = [...new Set(findOptionsByValue(outputs?.result.peak()))]
|
|
|
|
let css = initCss($app.css?.multiselectcomponent, customCss)
|
|
let outerDiv: HTMLDivElement | undefined = undefined
|
|
let portalRef: HTMLDivElement | undefined = undefined
|
|
let w = 0
|
|
let open: boolean = false
|
|
|
|
$: if (resolvedConfig.items) {
|
|
options = parseConfigOptions(resolvedConfig.items)
|
|
}
|
|
|
|
$componentControl[id] = {
|
|
setValue(newValues: any[]) {
|
|
selectOptionsByValue(newValues)
|
|
}
|
|
}
|
|
|
|
$: if (resolvedConfig.defaultItems) {
|
|
selectOptionsByValue(resolvedConfig.defaultItems)
|
|
}
|
|
|
|
$: outerDiv &&
|
|
portalRef &&
|
|
css?.multiselect?.style &&
|
|
setOuterDivStyle(outerDiv, portalRef, css?.multiselect?.style)
|
|
|
|
$: if (render && portalRef && outerDiv && options?.length > 0) {
|
|
tick().then(() => {
|
|
moveOptionsToPortal()
|
|
})
|
|
}
|
|
|
|
function findOptionsByValue(values: any[]) {
|
|
if (!Array.isArray(values)) {
|
|
return []
|
|
}
|
|
return options?.filter((item) => {
|
|
let itemValue = item.value
|
|
try {
|
|
if (typeof itemValue == 'string') {
|
|
itemValue = JSON.parse(itemValue)
|
|
}
|
|
} catch (_) {}
|
|
return values.some((value) => deepEqual(itemValue, value))
|
|
}) as ObjectOption[]
|
|
}
|
|
|
|
function selectOptionsByValue(values: any[]) {
|
|
if (!Array.isArray(values)) {
|
|
outputs?.result.set([])
|
|
}
|
|
|
|
selectedOptions = findOptionsByValue(values)
|
|
outputs?.result.set([...toValues(selectedOptions)])
|
|
}
|
|
|
|
function toValues(options: ObjectOption[] | undefined) {
|
|
if (!options || !Array.isArray(options)) {
|
|
return []
|
|
}
|
|
|
|
return options.map((o) => {
|
|
let val = o.value
|
|
try {
|
|
if (typeof val === 'string') val = JSON.parse(val)
|
|
} catch (_) {}
|
|
return val
|
|
})
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
</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 !selectedOptions || Array.isArray(selectedOptions)}
|
|
<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={selectedOptions}
|
|
{options}
|
|
placeholder={resolvedConfig.placeholder}
|
|
allowUserOptions={resolvedConfig.create}
|
|
key={(option) => {
|
|
if (typeof option == 'string' || typeof option == 'number') return option
|
|
return option.value
|
|
}}
|
|
on:change={(event) => {
|
|
if (event?.detail?.type === 'removeAll') {
|
|
outputs?.result.set([])
|
|
} else {
|
|
outputs?.result.set([...toValues(selectedOptions)])
|
|
}
|
|
}}
|
|
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)
|
|
}}
|
|
>
|
|
{@html option.label === '' ? ` ` : option.label}
|
|
</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 {selectedOptions} is not an array
|
|
{/if}
|
|
</div>
|
|
</AlignWrapper>
|