Rewrite options editor to use svelte store and be much cleaner

This commit is contained in:
Andrew Kingston 2024-05-23 15:16:19 +01:00
parent a9d9c170ce
commit 80af9042b0
1 changed files with 78 additions and 76 deletions

View File

@ -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}