Rewrite options editor to use svelte store and be much cleaner
This commit is contained in:
parent
a9d9c170ce
commit
80af9042b0
|
@ -2,101 +2,104 @@
|
||||||
import { flip } from "svelte/animate"
|
import { flip } from "svelte/animate"
|
||||||
import { dndzone } from "svelte-dnd-action"
|
import { dndzone } from "svelte-dnd-action"
|
||||||
import { Icon, Popover } from "@budibase/bbui"
|
import { Icon, Popover } from "@budibase/bbui"
|
||||||
import { onMount, tick } from "svelte"
|
import { tick } from "svelte"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
import { getSequentialName } from "helpers/duplicate"
|
import { getSequentialName } from "helpers/duplicate"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
export let constraints
|
export let constraints
|
||||||
export let optionColors = {}
|
export let optionColors = {}
|
||||||
|
|
||||||
const flipDurationMs = 150
|
const flipDurationMs = 130
|
||||||
const { OptionColours } = Constants
|
const { OptionColours } = Constants
|
||||||
|
|
||||||
let options = []
|
let openOption = null
|
||||||
let colorPopovers = []
|
let anchor = null
|
||||||
let anchors = []
|
let options = writable(
|
||||||
|
constraints.inclusion.map((value, idx) => ({
|
||||||
|
id: Math.random(),
|
||||||
|
name: value,
|
||||||
|
color: optionColors?.[value] || getDefaultColor(idx),
|
||||||
|
invalid: false,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
$: enrichedOptions = options.map((option, idx) => ({
|
$: options.subscribe(updateConstraints)
|
||||||
...option,
|
|
||||||
color: optionColors?.[option.name] || defaultColor(idx),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const defaultColor = idx => OptionColours[idx % OptionColours.length]
|
const updateConstraints = options => {
|
||||||
|
constraints.inclusion = options.map(option => option.name)
|
||||||
|
let newColors = {}
|
||||||
|
options.forEach(option => {
|
||||||
|
newColors[option.name] = option.color
|
||||||
|
})
|
||||||
|
optionColors = newColors
|
||||||
|
}
|
||||||
|
|
||||||
const removeInput = name => {
|
const getDefaultColor = idx => {
|
||||||
delete optionColors[name]
|
return OptionColours[idx % OptionColours.length]
|
||||||
constraints.inclusion = constraints.inclusion.filter(opt => opt !== name)
|
|
||||||
options = options.filter(opt => opt.name !== name)
|
|
||||||
colorPopovers.pop(undefined)
|
|
||||||
anchors.pop(undefined)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addNewInput = async () => {
|
const addNewInput = async () => {
|
||||||
const newName = getSequentialName(constraints.inclusion, "Option ", {
|
|
||||||
numberFirstItem: true,
|
|
||||||
})
|
|
||||||
const newId = Math.random()
|
const newId = Math.random()
|
||||||
options = [...options, { name: newName, id: newId }]
|
const newName = getSequentialName($options, "Option ", {
|
||||||
constraints.inclusion = [...constraints.inclusion, newName]
|
numberFirstItem: true,
|
||||||
optionColors[newName] = defaultColor(options.length - 1)
|
getName: option => option.name,
|
||||||
colorPopovers.push(undefined)
|
})
|
||||||
anchors.push(undefined)
|
options.update(state => {
|
||||||
|
return [
|
||||||
|
...state,
|
||||||
|
{
|
||||||
|
name: newName,
|
||||||
|
id: newId,
|
||||||
|
color: getDefaultColor(state.length),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Focus new option
|
||||||
await tick()
|
await tick()
|
||||||
document.getElementById(`option-${newId}`)?.focus()
|
document.getElementById(`option-${newId}`)?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDndConsider = e => {
|
const removeInput = id => {
|
||||||
options = e.detail.items
|
options.update(state => state.filter(option => option.id !== id))
|
||||||
}
|
|
||||||
const handleDndFinalize = e => {
|
|
||||||
options = e.detail.items
|
|
||||||
constraints.inclusion = options.map(option => option.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleColorChange = (name, color, idx) => {
|
const openColorPicker = id => {
|
||||||
optionColors[name] = color
|
anchor = document.getElementById(`color-${id}`)
|
||||||
colorPopovers[idx].hide()
|
openOption = id
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNameChange = (name, idx, newName) => {
|
const handleColorChange = (id, color) => {
|
||||||
|
options.update(state => {
|
||||||
|
state.find(option => option.id === id).color = color
|
||||||
|
return state.slice()
|
||||||
|
})
|
||||||
|
openOption = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNameChange = (id, newName) => {
|
||||||
// Check we don't already have this option
|
// Check we don't already have this option
|
||||||
const existing = options.some((option, optionIdx) => {
|
const existing = $options.some(option => {
|
||||||
return newName === option.name && idx !== optionIdx
|
return (option.name === newName) & (option.id !== id)
|
||||||
})
|
})
|
||||||
const invalid = !newName || existing
|
const invalid = !newName || existing
|
||||||
options.find(option => option.name === name).invalid = invalid
|
options.update(state => {
|
||||||
options = options.slice()
|
state.find(option => option.id === id).invalid = invalid
|
||||||
|
return state.slice()
|
||||||
|
})
|
||||||
|
|
||||||
// Stop if invalid or no change
|
// Stop if invalid
|
||||||
if (invalid || name === newName) {
|
if (invalid) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
constraints.inclusion[idx] = newName
|
// Update name
|
||||||
options[idx].name = newName
|
options.update(state => {
|
||||||
optionColors[newName] = optionColors[name]
|
state.find(option => option.id === id).name = newName
|
||||||
delete optionColors[name]
|
return state.slice()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const openColorPickerPopover = optionIdx => {
|
|
||||||
for (let i = 0; i < colorPopovers.length; i++) {
|
|
||||||
if (i === optionIdx) {
|
|
||||||
colorPopovers[i].show()
|
|
||||||
} else {
|
|
||||||
colorPopovers[i]?.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
// Initialize anchor arrays on mount, assuming 'options' is already populated
|
|
||||||
colorPopovers = constraints.inclusion.map(() => undefined)
|
|
||||||
anchors = constraints.inclusion.map(() => undefined)
|
|
||||||
options = constraints.inclusion.map(value => ({
|
|
||||||
id: Math.random(),
|
|
||||||
name: value,
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
@ -105,14 +108,14 @@
|
||||||
<div
|
<div
|
||||||
class="options"
|
class="options"
|
||||||
use:dndzone={{
|
use:dndzone={{
|
||||||
items: options,
|
items: $options,
|
||||||
flipDurationMs,
|
flipDurationMs,
|
||||||
dropTargetStyle: { outline: "none" },
|
dropTargetStyle: { outline: "none" },
|
||||||
}}
|
}}
|
||||||
on:consider={handleDndConsider}
|
on:consider={e => options.set(e.detail.items)}
|
||||||
on:finalize={handleDndFinalize}
|
on:finalize={e => options.set(e.detail.items)}
|
||||||
>
|
>
|
||||||
{#each enrichedOptions as option, idx (option.id)}
|
{#each $options as option (option.id)}
|
||||||
<div
|
<div
|
||||||
class="option"
|
class="option"
|
||||||
animate:flip={{ duration: flipDurationMs }}
|
animate:flip={{ duration: flipDurationMs }}
|
||||||
|
@ -122,14 +125,14 @@
|
||||||
<Icon name="DragHandle" size="L" />
|
<Icon name="DragHandle" size="L" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
bind:this={anchors[idx]}
|
id="color-{option.id}"
|
||||||
class="color-picker"
|
class="color-picker"
|
||||||
on:click={e => openColorPickerPopover(idx, e.target)}
|
on:click={() => openColorPicker(option.id)}
|
||||||
>
|
>
|
||||||
<div class="circle" style="--color:{option.color}">
|
<div class="circle" style="--color:{option.color}">
|
||||||
<Popover
|
<Popover
|
||||||
bind:this={colorPopovers[idx]}
|
open={openOption === option.id}
|
||||||
anchor={anchors[idx]}
|
{anchor}
|
||||||
align="left"
|
align="left"
|
||||||
offset={0}
|
offset={0}
|
||||||
animate={false}
|
animate={false}
|
||||||
|
@ -138,8 +141,7 @@
|
||||||
<div class="colors" data-ignore-click-outside="true">
|
<div class="colors" data-ignore-click-outside="true">
|
||||||
{#each OptionColours as colorOption}
|
{#each OptionColours as colorOption}
|
||||||
<div
|
<div
|
||||||
on:click={() =>
|
on:click={() => handleColorChange(option.id, colorOption)}
|
||||||
handleColorChange(option.name, colorOption, idx)}
|
|
||||||
style="--color:{colorOption};"
|
style="--color:{colorOption};"
|
||||||
class="circle"
|
class="circle"
|
||||||
class:selected={colorOption === option.color}
|
class:selected={colorOption === option.color}
|
||||||
|
@ -155,13 +157,13 @@
|
||||||
value={option.name}
|
value={option.name}
|
||||||
placeholder="Option name"
|
placeholder="Option name"
|
||||||
id="option-{option.id}"
|
id="option-{option.id}"
|
||||||
on:input={e => handleNameChange(option.name, idx, e.target.value)}
|
on:input={e => handleNameChange(option.id, e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
name="Close"
|
name="Close"
|
||||||
hoverable
|
hoverable
|
||||||
size="S"
|
size="S"
|
||||||
on:click={removeInput(option.name)}
|
on:click={() => removeInput(option.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
Loading…
Reference in New Issue