|
2 | 2 | import { getContext } from "svelte"; |
3 | 3 |
|
4 | 4 | import type { Editor } from "@graphite/editor"; |
5 | | - import type { LayoutTarget, WidgetInstance, WidgetSpanColumn, WidgetSpanRow } from "@graphite/messages"; |
6 | | - import { narrowWidgetProps, isWidgetSpanColumn, isWidgetSpanRow } from "@graphite/messages"; |
| 5 | + import type { LayoutTarget, WidgetInstance, WidgetPropsNames, WidgetPropsSet, WidgetTypes, WidgetSpanColumn, WidgetSpanRow } from "@graphite/messages"; |
| 6 | + import { isWidgetSpanColumn, isWidgetSpanRow } from "@graphite/messages"; |
7 | 7 | import { debouncer } from "@graphite/utility-functions/debounce"; |
8 | 8 |
|
9 | 9 | import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte"; |
|
71 | 71 | editor.handle.widgetValueCommitAndUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget); |
72 | 72 | } |
73 | 73 |
|
74 | | - // TODO: This seems to work, but verify the correctness and terseness of this, it's adapted from https://stackoverflow.com/a/67434028/775283 |
75 | | - function exclude<T extends object>(props: T, additional?: (keyof T)[]): Omit<T, typeof additional extends Array<infer K> ? K : never> { |
76 | | - const exclusions = ["kind", ...(additional || [])]; |
| 74 | + // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 75 | + function exclude(props: WidgetPropsSet, additional?: string[]): Record<string, any> { |
| 76 | + const exclusions = new Set(["kind", ...(additional || [])]); |
| 77 | + return Object.fromEntries(Object.entries(props).filter(([key]) => !exclusions.has(key))); |
| 78 | + } |
77 | 79 |
|
| 80 | + type WidgetConfig = { |
78 | 81 | // eslint-disable-next-line @typescript-eslint/no-explicit-any |
79 | | - return Object.fromEntries(Object.entries(props).filter((entry) => !exclusions.includes(entry[0]))) as any; |
80 | | - } |
81 | | -</script> |
| 82 | + component: any; |
| 83 | + // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 84 | + getProps(props: WidgetPropsSet, widgetIndex: number): Record<string, any> | undefined; |
| 85 | + getSlotContent?(props: WidgetPropsSet): string; |
| 86 | + }; |
82 | 87 |
|
83 | | -<!-- TODO: Refactor this component to use `<svelte:component this={attributesObject} />` to avoid all the separate conditional components --> |
| 88 | + const widgetRegistry: Record<WidgetPropsNames, WidgetConfig> = { |
| 89 | + CheckboxInput: { |
| 90 | + component: CheckboxInput, |
| 91 | + getProps: (props: WidgetTypes["CheckboxInput"], index) => ({ |
| 92 | + ...exclude(props), |
| 93 | + $$events: { checked: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) }, |
| 94 | + }), |
| 95 | + }, |
| 96 | + ColorInput: { |
| 97 | + component: ColorInput, |
| 98 | + getProps: (props: WidgetTypes["ColorInput"], index) => ({ |
| 99 | + ...exclude(props), |
| 100 | + $$events: { |
| 101 | + value: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false), |
| 102 | + startHistoryTransaction: () => widgetValueCommit(index, props.value), |
| 103 | + }, |
| 104 | + }), |
| 105 | + }, |
| 106 | + CurveInput: { |
| 107 | + // TODO: CurvesInput is currently unused |
| 108 | + component: CurveInput, |
| 109 | + getProps: (props: WidgetTypes["CurveInput"], index) => ({ |
| 110 | + ...exclude(props), |
| 111 | + $$events: { |
| 112 | + value: (e: CustomEvent) => debouncer((value: unknown) => widgetValueCommitAndUpdate(index, value, false), { debounceTime: 120 }).debounceUpdateValue(e.detail), |
| 113 | + }, |
| 114 | + }), |
| 115 | + }, |
| 116 | + DropdownInput: { |
| 117 | + component: DropdownInput, |
| 118 | + getProps: (props: WidgetTypes["DropdownInput"], index) => ({ |
| 119 | + ...exclude(props), |
| 120 | + $$events: { |
| 121 | + hoverInEntry: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false), |
| 122 | + hoverOutEntry: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false), |
| 123 | + selectedIndex: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true), |
| 124 | + }, |
| 125 | + }), |
| 126 | + }, |
| 127 | + ParameterExposeButton: { |
| 128 | + component: ParameterExposeButton, |
| 129 | + getProps: (props: WidgetTypes["ParameterExposeButton"], index) => ({ |
| 130 | + ...exclude(props), |
| 131 | + action: () => widgetValueCommitAndUpdate(index, undefined, true), |
| 132 | + }), |
| 133 | + }, |
| 134 | + IconButton: { |
| 135 | + component: IconButton, |
| 136 | + getProps: (props: WidgetTypes["IconButton"], index) => ({ |
| 137 | + ...exclude(props), |
| 138 | + action: () => widgetValueCommitAndUpdate(index, undefined, true), |
| 139 | + }), |
| 140 | + }, |
| 141 | + IconLabel: { |
| 142 | + component: IconLabel, |
| 143 | + getProps: (props: WidgetTypes["IconLabel"]) => exclude(props), |
| 144 | + }, |
| 145 | + ShortcutLabel: { |
| 146 | + component: ShortcutLabel, |
| 147 | + getProps: (props: WidgetTypes["ShortcutLabel"]) => { |
| 148 | + if (!props.shortcut) return undefined; |
| 149 | + return exclude(props); |
| 150 | + }, |
| 151 | + }, |
| 152 | + ImageLabel: { |
| 153 | + component: ImageLabel, |
| 154 | + getProps: (props: WidgetTypes["ImageLabel"]) => exclude(props), |
| 155 | + }, |
| 156 | + ImageButton: { |
| 157 | + component: ImageButton, |
| 158 | + getProps: (props: WidgetTypes["ImageButton"], index) => ({ |
| 159 | + ...exclude(props), |
| 160 | + action: () => widgetValueCommitAndUpdate(index, undefined, true), |
| 161 | + }), |
| 162 | + }, |
| 163 | + NodeCatalog: { |
| 164 | + component: NodeCatalog, |
| 165 | + getProps: (props: WidgetTypes["NodeCatalog"], index) => ({ |
| 166 | + ...exclude(props), |
| 167 | + $$events: { selectNodeType: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, false) }, |
| 168 | + }), |
| 169 | + }, |
| 170 | + NumberInput: { |
| 171 | + component: NumberInput, |
| 172 | + getProps: (props: WidgetTypes["NumberInput"], index) => ({ |
| 173 | + ...exclude(props), |
| 174 | + incrementCallbackIncrease: () => widgetValueCommitAndUpdate(index, "Increment", false), |
| 175 | + incrementCallbackDecrease: () => widgetValueCommitAndUpdate(index, "Decrement", false), |
| 176 | + $$events: { |
| 177 | + value: (e: CustomEvent) => debouncer((value: unknown) => widgetValueUpdate(index, value, true)).debounceUpdateValue(e.detail), |
| 178 | + startHistoryTransaction: () => widgetValueCommit(index, props.value), |
| 179 | + }, |
| 180 | + }), |
| 181 | + }, |
| 182 | + ReferencePointInput: { |
| 183 | + component: ReferencePointInput, |
| 184 | + getProps: (props: WidgetTypes["ReferencePointInput"], index) => ({ |
| 185 | + ...exclude(props), |
| 186 | + $$events: { value: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) }, |
| 187 | + }), |
| 188 | + }, |
| 189 | + PopoverButton: { |
| 190 | + component: PopoverButton, |
| 191 | + getProps: (props: WidgetTypes["PopoverButton"]) => ({ |
| 192 | + ...exclude(props), |
| 193 | + layoutTarget, |
| 194 | + }), |
| 195 | + }, |
| 196 | + RadioInput: { |
| 197 | + component: RadioInput, |
| 198 | + getProps: (props: WidgetTypes["RadioInput"], index) => ({ |
| 199 | + ...exclude(props), |
| 200 | + $$events: { selectedIndex: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) }, |
| 201 | + }), |
| 202 | + }, |
| 203 | + Separator: { |
| 204 | + component: Separator, |
| 205 | + getProps: (props: WidgetTypes["Separator"]) => exclude(props), |
| 206 | + }, |
| 207 | + WorkingColorsInput: { |
| 208 | + component: WorkingColorsInput, |
| 209 | + getProps: (props: WidgetTypes["WorkingColorsInput"]) => exclude(props), |
| 210 | + }, |
| 211 | + TextAreaInput: { |
| 212 | + component: TextAreaInput, |
| 213 | + getProps: (props: WidgetTypes["TextAreaInput"], index) => ({ |
| 214 | + ...exclude(props), |
| 215 | + $$events: { commitText: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, false) }, |
| 216 | + }), |
| 217 | + }, |
| 218 | + TextButton: { |
| 219 | + component: TextButton, |
| 220 | + getProps: (props: WidgetTypes["TextButton"], index) => ({ |
| 221 | + ...exclude(props), |
| 222 | + action: () => widgetValueCommitAndUpdate(index, [], true), |
| 223 | + $$events: { selectedEntryValuePath: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, false) }, |
| 224 | + }), |
| 225 | + }, |
| 226 | + BreadcrumbTrailButtons: { |
| 227 | + component: BreadcrumbTrailButtons, |
| 228 | + getProps: (props: WidgetTypes["BreadcrumbTrailButtons"], index) => ({ |
| 229 | + ...exclude(props), |
| 230 | + action: (breadcrumbIndex: number) => widgetValueCommitAndUpdate(index, breadcrumbIndex, true), |
| 231 | + }), |
| 232 | + }, |
| 233 | + TextInput: { |
| 234 | + component: TextInput, |
| 235 | + getProps: (props: WidgetTypes["TextInput"], index) => ({ |
| 236 | + ...exclude(props), |
| 237 | + $$events: { commitText: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) }, |
| 238 | + }), |
| 239 | + }, |
| 240 | + TextLabel: { |
| 241 | + component: TextLabel, |
| 242 | + getProps: (props: WidgetTypes["TextLabel"]) => exclude(props, ["value"]), |
| 243 | + getSlotContent: (props: WidgetTypes["TextLabel"]) => props.value, |
| 244 | + }, |
| 245 | + }; |
| 246 | +</script> |
84 | 247 |
|
85 | 248 | <div class={`widget-span ${className} ${extraClasses}`.trim()} class:narrow class:row={direction === "row"} class:column={direction === "column"}> |
86 | | - {#each widgets as component, widgetIndex} |
87 | | - {@const checkboxInput = narrowWidgetProps(component.props, "CheckboxInput")} |
88 | | - {#if checkboxInput} |
89 | | - <CheckboxInput {...exclude(checkboxInput)} on:checked={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} /> |
90 | | - {/if} |
91 | | - {@const colorInput = narrowWidgetProps(component.props, "ColorInput")} |
92 | | - {#if colorInput} |
93 | | - <ColorInput |
94 | | - {...exclude(colorInput)} |
95 | | - on:value={({ detail }) => widgetValueUpdate(widgetIndex, detail, false)} |
96 | | - on:startHistoryTransaction={() => widgetValueCommit(widgetIndex, colorInput.value)} |
97 | | - /> |
98 | | - {/if} |
99 | | - <!-- TODO: Curves Input is currently unused --> |
100 | | - {@const curvesInput = narrowWidgetProps(component.props, "CurveInput")} |
101 | | - {#if curvesInput} |
102 | | - <CurveInput |
103 | | - {...exclude(curvesInput)} |
104 | | - on:value={({ detail }) => debouncer((value) => widgetValueCommitAndUpdate(widgetIndex, value, false), { debounceTime: 120 }).debounceUpdateValue(detail)} |
105 | | - /> |
106 | | - {/if} |
107 | | - {@const dropdownInput = narrowWidgetProps(component.props, "DropdownInput")} |
108 | | - {#if dropdownInput} |
109 | | - <DropdownInput |
110 | | - {...exclude(dropdownInput)} |
111 | | - on:hoverInEntry={({ detail }) => { |
112 | | - return widgetValueUpdate(widgetIndex, detail, false); |
113 | | - }} |
114 | | - on:hoverOutEntry={({ detail }) => { |
115 | | - return widgetValueUpdate(widgetIndex, detail, false); |
116 | | - }} |
117 | | - on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} |
118 | | - /> |
119 | | - {/if} |
120 | | - {@const parameterExposeButton = narrowWidgetProps(component.props, "ParameterExposeButton")} |
121 | | - {#if parameterExposeButton} |
122 | | - <ParameterExposeButton {...exclude(parameterExposeButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} /> |
123 | | - {/if} |
124 | | - {@const iconButton = narrowWidgetProps(component.props, "IconButton")} |
125 | | - {#if iconButton} |
126 | | - <IconButton {...exclude(iconButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} /> |
127 | | - {/if} |
128 | | - {@const iconLabel = narrowWidgetProps(component.props, "IconLabel")} |
129 | | - {#if iconLabel} |
130 | | - <IconLabel {...exclude(iconLabel)} /> |
131 | | - {/if} |
132 | | - {@const shortcutLabel = narrowWidgetProps(component.props, "ShortcutLabel")} |
133 | | - {@const shortcutLabelShortcut = shortcutLabel?.shortcut ? { ...shortcutLabel, shortcut: shortcutLabel.shortcut } : undefined} |
134 | | - {#if shortcutLabel && shortcutLabelShortcut} |
135 | | - <ShortcutLabel {...exclude(shortcutLabelShortcut)} /> |
136 | | - {/if} |
137 | | - {@const imageLabel = narrowWidgetProps(component.props, "ImageLabel")} |
138 | | - {#if imageLabel} |
139 | | - <ImageLabel {...exclude(imageLabel)} /> |
140 | | - {/if} |
141 | | - {@const imageButton = narrowWidgetProps(component.props, "ImageButton")} |
142 | | - {#if imageButton} |
143 | | - <ImageButton {...exclude(imageButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} /> |
144 | | - {/if} |
145 | | - {@const nodeCatalog = narrowWidgetProps(component.props, "NodeCatalog")} |
146 | | - {#if nodeCatalog} |
147 | | - <NodeCatalog {...exclude(nodeCatalog)} on:selectNodeType={(e) => widgetValueCommitAndUpdate(widgetIndex, e.detail, false)} /> |
148 | | - {/if} |
149 | | - {@const numberInput = narrowWidgetProps(component.props, "NumberInput")} |
150 | | - {#if numberInput} |
151 | | - <NumberInput |
152 | | - {...exclude(numberInput)} |
153 | | - on:value={({ detail }) => debouncer((value) => widgetValueUpdate(widgetIndex, value, true)).debounceUpdateValue(detail)} |
154 | | - on:startHistoryTransaction={() => widgetValueCommit(widgetIndex, numberInput.value)} |
155 | | - incrementCallbackIncrease={() => widgetValueCommitAndUpdate(widgetIndex, "Increment", false)} |
156 | | - incrementCallbackDecrease={() => widgetValueCommitAndUpdate(widgetIndex, "Decrement", false)} |
157 | | - /> |
158 | | - {/if} |
159 | | - {@const referencePointInput = narrowWidgetProps(component.props, "ReferencePointInput")} |
160 | | - {#if referencePointInput} |
161 | | - <ReferencePointInput {...exclude(referencePointInput)} on:value={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} /> |
162 | | - {/if} |
163 | | - {@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")} |
164 | | - {#if popoverButton} |
165 | | - <PopoverButton {...exclude(popoverButton)} {layoutTarget} /> |
166 | | - {/if} |
167 | | - {@const radioInput = narrowWidgetProps(component.props, "RadioInput")} |
168 | | - {#if radioInput} |
169 | | - <RadioInput {...exclude(radioInput)} on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} /> |
170 | | - {/if} |
171 | | - {@const separator = narrowWidgetProps(component.props, "Separator")} |
172 | | - {#if separator} |
173 | | - <Separator {...exclude(separator)} /> |
174 | | - {/if} |
175 | | - {@const workingColorsInput = narrowWidgetProps(component.props, "WorkingColorsInput")} |
176 | | - {#if workingColorsInput} |
177 | | - <WorkingColorsInput {...exclude(workingColorsInput)} /> |
178 | | - {/if} |
179 | | - {@const textAreaInput = narrowWidgetProps(component.props, "TextAreaInput")} |
180 | | - {#if textAreaInput} |
181 | | - <TextAreaInput {...exclude(textAreaInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, false)} /> |
182 | | - {/if} |
183 | | - {@const textButton = narrowWidgetProps(component.props, "TextButton")} |
184 | | - {#if textButton} |
185 | | - <TextButton |
186 | | - {...exclude(textButton)} |
187 | | - action={() => widgetValueCommitAndUpdate(widgetIndex, [], true)} |
188 | | - on:selectedEntryValuePath={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, false)} |
189 | | - /> |
190 | | - {/if} |
191 | | - {@const breadcrumbTrailButtons = narrowWidgetProps(component.props, "BreadcrumbTrailButtons")} |
192 | | - {#if breadcrumbTrailButtons} |
193 | | - <BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} action={(breadcrumbIndex) => widgetValueCommitAndUpdate(widgetIndex, breadcrumbIndex, true)} /> |
194 | | - {/if} |
195 | | - {@const textInput = narrowWidgetProps(component.props, "TextInput")} |
196 | | - {#if textInput} |
197 | | - <TextInput {...exclude(textInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} /> |
198 | | - {/if} |
199 | | - {@const textLabel = narrowWidgetProps(component.props, "TextLabel")} |
200 | | - {#if textLabel} |
201 | | - <TextLabel {...exclude(textLabel, ["value"])}>{textLabel.value}</TextLabel> |
| 249 | + {#each widgets as widget, widgetIndex} |
| 250 | + {@const config = widgetRegistry[widget.props.kind]} |
| 251 | + {@const props = config?.getProps(widget.props, widgetIndex)} |
| 252 | + {@const slot = config?.getSlotContent?.(widget.props)} |
| 253 | + {#if props !== undefined && slot !== undefined} |
| 254 | + <svelte:component this={config.component} {...props}>{slot}</svelte:component> |
| 255 | + {:else if props !== undefined} |
| 256 | + <svelte:component this={config.component} {...props} /> |
202 | 257 | {/if} |
203 | 258 | {/each} |
204 | 259 | </div> |
|
213 | 268 | .widget-span.row { |
214 | 269 | flex: 0 0 auto; |
215 | 270 | display: flex; |
216 | | - --row-height: 32px; |
217 | 271 | min-height: var(--row-height); |
| 272 | + --row-height: 32px; |
218 | 273 |
|
219 | 274 | &.narrow { |
220 | 275 | --row-height: 24px; |
|
0 commit comments