Fix FieldInput and several other Svelte bugs

This commit is contained in:
Keavon Chambers
2023-03-08 01:35:22 -08:00
parent 2e3495aa0e
commit 7ce9d6db05
9 changed files with 56 additions and 25 deletions
+7
View File
@@ -0,0 +1,7 @@
Copyright (c) 2021-2023 Keavon Chambers
The design assets in this directory (including SVG code for icons and logos) are NOT licensed under the Apache 2.0 license terms applied to other Graphite source code files. This directory and its entire contents are excluded from the Apache 2.0 source code license, and copyrights are held by the author for the creative works contained as files herein.
Parties interested in using Graphite source code in a capacity that deploys the Graphite Editor reference frontend are advised to substitute all assets and "Graphite" branding or otherwise arrange written permission from the rightsholder. The recommended use case for adopting Graphite open source code is to develop one's own unique frontend user interface implementation that integrates Graphite's backend technology.
The author and rightsholder, Keavon Chambers, may be reached through the email address listed at https://graphite.rs/contact/ or https://keavon.com.
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path class="bright" d="M6,1C4.3,1,3,2.3,3,4v4h5V1H6z M7,6v1H4c0,0,0-0.6,0.2-1c0.3-0.6,0.8-1.1,1.3-1.4c1.1-0.9,0.8-1.9,0.1-1.7C5,3,4.8,3.6,4.9,4.1H4C3.9,2.9,4.7,2,5.8,2c1.1,0,1.5,1.2,1,2.3C6.4,5.1,5.5,5.5,5.4,6H7z" />
<path class="dim" d="M10,1H9v1h1c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V9H3v1c0,2.76,2.24,5,5,5s5-2.24,5-5V4C13,2.35,11.65,1,10,1z" />
</svg>

After

Width:  |  Height:  |  Size: 432 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path class="bright" d="M10,1H8v7h5V4C13,2.3,11.7,1,10,1z M12,7H9.1c0,0,0-0.6,0.2-1c0.2-0.6,0.8-1.1,1.3-1.5c0.9-0.8,0.4-1.8-0.4-1.6C9.6,3.1,9.9,4.1,9.9,4.1H9.1c-0.3-1,0.1-2.2,1.3-2.1c1,0.1,1.6,1.1,1.4,2.1C11.6,5,10.6,5.4,10.5,6H12V7z" />
<path class="dim" d="M6,1h1v1H6C4.9,2,4,2.9,4,4v6c0,2.21,1.79,4,4,4s4-1.79,4-4V9h1v1c0,2.76-2.24,5-5,5s-5-2.24-5-5V4C3,2.35,4.35,1,6,1z" />
</svg>

After

Width:  |  Height:  |  Size: 448 B

@@ -320,7 +320,10 @@
{/if} {/if}
<NumberInput <NumberInput
value={strength} value={strength}
on:value={({ detail }) => setColorRGB(channel, detail)} on:value={({ detail }) => {
strength = detail;
setColorRGB(channel, detail);
}}
min={0} min={0}
max={255} max={255}
minWidth={56} minWidth={56}
@@ -341,7 +344,10 @@
{/if} {/if}
<NumberInput <NumberInput
value={strength} value={strength}
on:value={({ detail }) => setColorHSV(channel, detail)} on:value={({ detail }) => {
strength = detail;
setColorHSV(channel, detail);
}}
min={0} min={0}
max={channel === "h" ? 360 : 100} max={channel === "h" ? 360 : 100}
unit={channel === "h" ? "°" : "%"} unit={channel === "h" ? "°" : "%"}
@@ -358,7 +364,10 @@
<NumberInput <NumberInput
label="Alpha" label="Alpha"
value={!isNone ? alpha * 100 : undefined} value={!isNone ? alpha * 100 : undefined}
on:value={({ detail }) => setColorAlphaPercent(detail)} on:value={({ detail }) => {
if (detail !== undefined) alpha = detail / 100;
setColorAlphaPercent(detail);
}}
min={0} min={0}
max={100} max={100}
rangeMin={0} rangeMin={0}
@@ -59,7 +59,7 @@
return sparse; return sparse;
} }
function buildNodeCategories(nodeTypes: FrontendNodeType[], searchTerm: string) { function buildNodeCategories(nodeTypes: FrontendNodeType[], searchTerm: string): [string, FrontendNodeType[]][] {
const categories = new Map(); const categories = new Map();
nodeTypes.forEach((node) => { nodeTypes.forEach((node) => {
if (searchTerm.length > 0 && !node.name.toLowerCase().includes(searchTerm.toLowerCase()) && !node.category.toLowerCase().includes(searchTerm.toLowerCase())) { if (searchTerm.length > 0 && !node.name.toLowerCase().includes(searchTerm.toLowerCase()) && !node.category.toLowerCase().includes(searchTerm.toLowerCase())) {
@@ -488,10 +488,10 @@
{#if nodeListLocation} {#if nodeListLocation}
<LayoutCol class="node-list" data-node-list styles={{ "margin-left": `${nodeListX}px`, "margin-top": `${nodeListY}px` }}> <LayoutCol class="node-list" data-node-list styles={{ "margin-left": `${nodeListX}px`, "margin-top": `${nodeListY}px` }}>
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} /> <TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
{#each nodeCategories as nodeCategory (nodeCategory[0])} {#each nodeCategories as nodeCategory}
<LayoutCol> <LayoutCol>
<TextLabel>{nodeCategory[0]}</TextLabel> <TextLabel>{nodeCategory[0]}</TextLabel>
{#each nodeCategory[1] as nodeType (String(nodeType))} {#each nodeCategory[1] as nodeType}
<TextButton label={nodeType.name} action={() => createNode(nodeType.name)} /> <TextButton label={nodeType.name} action={() => createNode(nodeType.name)} />
{/each} {/each}
</LayoutCol> </LayoutCol>
@@ -121,7 +121,7 @@
{#if numberInput} {#if numberInput}
<NumberInput <NumberInput
{...exclude(numberInput)} {...exclude(numberInput)}
on:value={({ detail }) => debouncer(() => updateLayout(index, detail))} on:value={({ detail }) => debouncer((value) => updateLayout(index, value)).updateValue(detail)}
incrementCallbackIncrease={() => updateLayout(index, "Increment")} incrementCallbackIncrease={() => updateLayout(index, "Increment")}
incrementCallbackDecrease={() => updateLayout(index, "Decrement")} incrementCallbackDecrease={() => updateLayout(index, "Decrement")}
sharpRightCorners={nextIsSuffix} sharpRightCorners={nextIsSuffix}
@@ -31,8 +31,8 @@
let inputOrTextarea: HTMLInputElement | HTMLTextAreaElement; let inputOrTextarea: HTMLInputElement | HTMLTextAreaElement;
let id = `${Math.random()}`.substring(2); let id = `${Math.random()}`.substring(2);
let macKeyboardLayout = platformIsMac(); let macKeyboardLayout = platformIsMac();
let inputValue = value;
$: inputValue = value;
$: dispatch("value", inputValue); $: dispatch("value", inputValue);
// Select (highlight) all the text. For technical reasons, it is necessary to pass the current text. // Select (highlight) all the text. For technical reasons, it is necessary to pass the current text.
@@ -52,7 +52,7 @@
export let incrementCallbackDecrease: (() => void) | undefined = undefined; export let incrementCallbackDecrease: (() => void) | undefined = undefined;
let self: FieldInput; let self: FieldInput;
let text = displayText(value); let text = displayText(value, displayDecimalPlaces, unit);
let editing = false; let editing = false;
// Stays in sync with a binding to the actual input range slider element. // Stays in sync with a binding to the actual input range slider element.
let rangeSliderValue = value !== undefined ? value : 0; let rangeSliderValue = value !== undefined ? value : 0;
@@ -84,10 +84,10 @@
if (typeof min === "number") sanitized = Math.max(sanitized, min); if (typeof min === "number") sanitized = Math.max(sanitized, min);
if (typeof max === "number") sanitized = Math.min(sanitized, max); if (typeof max === "number") sanitized = Math.min(sanitized, max);
text = displayText(sanitized); text = displayText(sanitized, displayDecimalPlaces, unit);
} }
function sliderInput() { function onSliderInput() {
// Keep only 4 digits after the decimal point // Keep only 4 digits after the decimal point
const ROUNDING_EXPONENT = 4; const ROUNDING_EXPONENT = 4;
const ROUNDING_MAGNITUDE = 10 ** ROUNDING_EXPONENT; const ROUNDING_MAGNITUDE = 10 ** ROUNDING_EXPONENT;
@@ -113,10 +113,10 @@
// If we're in a dragging state, we want to use the new slider value // If we're in a dragging state, we want to use the new slider value
rangeSliderValueAsRendered = roundedValue; rangeSliderValueAsRendered = roundedValue;
updateValue(roundedValue); updateValue(roundedValue, min, max, displayDecimalPlaces, unit);
} }
function sliderPointerDown() { function onSliderPointerDown() {
// We want to render the fake slider thumb at the old position, which is still the number held by `value` // We want to render the fake slider thumb at the old position, which is still the number held by `value`
rangeSliderValueAsRendered = value || 0; rangeSliderValueAsRendered = value || 0;
@@ -124,7 +124,7 @@
// invocation will transition the state machine to `mousedown`. That's why we don't do it here. // invocation will transition the state machine to `mousedown`. That's why we don't do it here.
} }
function sliderPointerUp() { function onSliderPointerUp() {
// User clicked but didn't drag, so we focus the text input element // User clicked but didn't drag, so we focus the text input element
if (rangeSliderClickDragState === "mousedown") { if (rangeSliderClickDragState === "mousedown") {
const inputElement = self.element().querySelector("[data-input-element]") as HTMLInputElement | undefined; const inputElement = self.element().querySelector("[data-input-element]") as HTMLInputElement | undefined;
@@ -160,7 +160,7 @@
const parsed = parseFloat(text); const parsed = parseFloat(text);
const newValue = Number.isNaN(parsed) ? undefined : parsed; const newValue = Number.isNaN(parsed) ? undefined : parsed;
updateValue(newValue); updateValue(newValue, min, max, displayDecimalPlaces, unit);
editing = false; editing = false;
@@ -168,7 +168,7 @@
} }
function onCancelTextChange() { function onCancelTextChange() {
updateValue(undefined); updateValue(undefined, min, max, displayDecimalPlaces, unit);
editing = false; editing = false;
@@ -181,11 +181,11 @@
const actions: Record<NumberInputIncrementBehavior, () => void> = { const actions: Record<NumberInputIncrementBehavior, () => void> = {
Add: () => { Add: () => {
const directionAddend = direction === "Increase" ? step : -step; const directionAddend = direction === "Increase" ? step : -step;
updateValue(value !== undefined ? value + directionAddend : undefined); updateValue(value !== undefined ? value + directionAddend : undefined, min, max, displayDecimalPlaces, unit);
}, },
Multiply: () => { Multiply: () => {
const directionMultiplier = direction === "Increase" ? step : 1 / step; const directionMultiplier = direction === "Increase" ? step : 1 / step;
updateValue(value !== undefined ? value * directionMultiplier : undefined); updateValue(value !== undefined ? value * directionMultiplier : undefined, min, max, displayDecimalPlaces, unit);
}, },
Callback: () => { Callback: () => {
if (direction === "Increase") incrementCallbackIncrease?.(); if (direction === "Increase") incrementCallbackIncrease?.();
@@ -197,20 +197,20 @@
action(); action();
} }
function updateValue(newValue: number | undefined) { function updateValue(newValue: number | undefined, min: number | undefined, max: number | undefined, displayDecimalPlaces: number, unit: string) {
// Check if the new value is valid, otherwise we use the old value (rounded if it's an integer)
const nowValid = value !== undefined && isInteger ? Math.round(value) : value; const nowValid = value !== undefined && isInteger ? Math.round(value) : value;
let cleaned = newValue !== undefined ? newValue : nowValid; let cleaned = newValue !== undefined ? newValue : nowValid;
if (typeof min === "number" && !Number.isNaN(min) && cleaned !== undefined) cleaned = Math.max(cleaned, min); if (typeof min === "number" && !Number.isNaN(min) && cleaned !== undefined) cleaned = Math.max(cleaned, min);
if (typeof max === "number" && !Number.isNaN(max) && cleaned !== undefined) cleaned = Math.min(cleaned, max); if (typeof max === "number" && !Number.isNaN(max) && cleaned !== undefined) cleaned = Math.min(cleaned, max);
// Required as the call to update:value can, not change the value text = displayText(cleaned, displayDecimalPlaces, unit);
text = displayText(value);
if (newValue !== undefined) dispatch("value", cleaned); if (newValue !== undefined) dispatch("value", cleaned);
} }
function displayText(value: number | undefined): string { function displayText(value: number | undefined, displayDecimalPlaces: number, unit: string): string {
if (value === undefined) return "-"; if (value === undefined) return "-";
// Find the amount of digits on the left side of the decimal // Find the amount of digits on the left side of the decimal
@@ -261,9 +261,9 @@
max={rangeMax} max={rangeMax}
step={sliderStepValue} step={sliderStepValue}
{disabled} {disabled}
on:input={sliderInput} on:input={onSliderInput}
on:pointerdown={sliderPointerDown} on:pointerdown={onSliderPointerDown}
on:pointerup={sliderPointerUp} on:pointerup={onSliderPointerUp}
tabindex="-1" tabindex="-1"
/> />
{/if} {/if}
+7
View File
@@ -0,0 +1,7 @@
Copyright (c) 2021-2023 Keavon Chambers
The design assets in this directory (including SVG code for icons and logos) are NOT licensed under the Apache 2.0 license terms applied to other Graphite source code files. This directory and its entire contents are excluded from the Apache 2.0 source code license, and copyrights are held by the author for the creative works contained as files herein.
Parties interested in using Graphite source code in a capacity that deploys the Graphite Editor reference frontend are advised to substitute all assets and "Graphite" branding or otherwise arrange written permission from the rightsholder. The recommended use case for adopting Graphite open source code is to develop one's own unique frontend user interface implementation that integrates Graphite's backend technology.
The author and rightsholder, Keavon Chambers, may be reached through the email address listed at https://graphite.rs/contact/ or https://keavon.com.