Skip to content

Commit

Permalink
Merge pull request #8 from twm/widgets
Browse files Browse the repository at this point in the history
Improved form widgets
  • Loading branch information
twm authored Sep 8, 2024
2 parents d33f5aa + a57d13e commit 2533b19
Show file tree
Hide file tree
Showing 12 changed files with 307 additions and 97 deletions.
16 changes: 0 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,5 @@
"vite": "^5.0.3",
"vitest": "^2.0.0"
},
"type": "module",
"dependencies": {
"fraction.js": "^4.3.7"
}
"type": "module"
}
7 changes: 0 additions & 7 deletions src/index.test.ts

This file was deleted.

83 changes: 38 additions & 45 deletions src/lib/Form.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,75 +13,68 @@
MATERIALS,
} from "$lib/stores"
import { frac } from "$lib/frac"
import FracInput from "$lib/FracInput.svelte"
import FracRange from "$lib/FracRange.svelte"
</script>

<h2>Inputs</h2>

<form action="#" method="GET">
<label>
Art width
<input type="number" bind:value={$artX} min="1" step="0.25" required />
</label>
<label>
Art height
<input type="number" bind:value={$artY} min="1" step="0.25" required />
</label>
<label for="art-width"> Art width </label>
<FracInput id="art-width" bind:value={$artX} min={1} required />
<label for="art-height"> Art height </label>
<FracInput id="art-height" bind:value={$artY} min={1} required />

<label>
Frame width
<input
type="number"
bind:value={$frameWidth}
max={$frameWidthMax}
min={$frameWidthMin}
step="0.125"
required
/>
</label>
<label>
Rabbet width
<input
type="number"
bind:value={$rabbetWidth}
max={$rabbetWidthMax}
min={$rabbetWidthMin}
step="0.125"
/>
</label>
<label for="frame-width"> Frame width </label>

<label>
Material
<select bind:value={$material}>
{#each MATERIALS as material}
<option value={material}>{material.name}</option>
{/each}
</select>
</label>
<FracRange
id="frame-width"
bind:value={$frameWidth}
max={$frameWidthMax}
min={$frameWidthMin}
step={0.125}
required
/>
<label for="rabbet-width"> Rabbet width </label>
<FracRange
id="rabbet-width"
bind:value={$rabbetWidth}
max={$rabbetWidthMax}
min={$rabbetWidthMin}
step={0.125}
required
/>

<label for="material"> Material </label>
<select id="material" bind:value={$material}>
{#each MATERIALS as material}
<option value={material}>{material.name}</option>
{/each}
</select>
</form>

<h2>Outputs</h2>

<div class="outputs">
<p class="label">Linear stock <span>{frac($stockLength)}"</span></p>
<label for="linear-stock">Linear stock </label>
<output id="linear-stock">{frac($stockLength)}"</output>
</div>

<style>
form,
.outputs {
display: grid;
grid-template-columns: 1fr 8rem;
grid-template-columns: 1fr 12rem;
gap: 0.5rem;
}
label,
.label {
display: contents;
}
input,
select {
box-sizing: border-box;
width: 100%;
background: inherit;
color: inherit;
border: 1px solid currentColor;
border: none;
border-bottom: 1px solid currentColor;
padding: 1px 4px;
line-height: 1;
}
Expand Down
85 changes: 85 additions & 0 deletions src/lib/FracInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<script lang="ts">
import { frac, parseFrac } from "$lib/frac"
function validate(
input: HTMLInputElement,
v: number,
min: number | null,
max: number | null
) {
if (isNaN(v)) {
input.setCustomValidity("Enter a mixed fraction or decimal number")
return false
}
if (min != null && v < min) {
input.setCustomValidity(`Must be at least ${frac(min)}`)
return false
}
if (max != null && v > max) {
input.setCustomValidity(`Must not exceed ${frac(max)}`)
return false
}
input.setCustomValidity("")
return true
}
export let id: string
export let value: number
export let min: number | null = null
export let max: number | null = null
export let required: boolean = false
let focused: boolean = false
let input: HTMLInputElement
let rawValue = frac(value)
let displayValue = frac(value) + '"'
$: {
// Round to the nearest 1⁄32nd to match the masked value.
let v = Math.round(parseFrac(rawValue) * 32) / 32
if (input) {
if (validate(input, v, min, max)) {
value = v
displayValue = frac(v) + '"'
} else {
displayValue = rawValue
}
}
}
</script>

<span class="range">
<input
type="text"
{id}
value={focused ? rawValue : displayValue}
bind:this={input}
{required}
on:input={() => {
rawValue = input.value
}}
on:focus={() => {
focused = true
}}
on:blur={() => {
focused = false
}}
/>
</span>

<style>
input {
box-sizing: border-box;
width: 100%;
background: inherit;
color: inherit;
border: none;
border-bottom: 1px solid currentColor;
padding: 0;
line-height: 1;
}
:invalid {
border-bottom-color: red;
}
</style>
30 changes: 30 additions & 0 deletions src/lib/FracRange.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script lang="ts">
import { frac } from "$lib/frac"
export let id: string
export let value: number
export let min: number
export let max: number
export let step: number
export let required: boolean = false
</script>

<span class="range">
<output for={id}>{frac(value)}"</output>
<input type="range" {id} bind:value {min} {max} {step} {required} />
</span>

<style>
.range {
display: grid;
grid-template-columns: 3rem 1fr;
}
input {
width: 100%;
box-sizing: border-box;
background: inherit;
color: inherit;
border: none;
}
</style>
71 changes: 71 additions & 0 deletions src/lib/frac.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, test, expect } from "vitest"
import { frac, parseFrac } from "$lib/frac"

describe("frac renders numbers as mixed fractions", () => {
const cases: [number, string][] = [
[-1 - 3 / 8, "-1\u20093⁄8"],
[-1, "-1"],
[0, "0"],
[1, "1"],
[1.5, "1\u20091⁄2"],
[12.25, "12\u20091⁄4"],
[-6.125, "-6\u20091⁄8"],
[1 / 4, "1⁄4"],
[7 / 8, "7⁄8"],
[15 / 16, "15⁄16"],
[1 + 17 / 32, "1\u200917⁄32"],
[23 / 32, "23⁄32"],
[3 + 61.5 / 64, "3\u200931⁄32"],
]
test.for(cases)("renders %d as %s", ([n, s]) => {
expect(frac(n)).toEqual(s)
})
})

describe("parseFrac converts strings to numbers", () => {
const cases: [string, number][] = [
// Whole numbers
["1", 1],
["1.", 1],
["0", 0],
["-1", -1],
["-1.", -1],
["-1.0", -1],
// Decimal numbers
["1.25", 1.25],
["+1.25", 1.25],
["+12.25", 12.25],
// Vulgar fractions
["1/64", 1 / 64],
["5/32", 5 / 32],
["-1/4", -0.25],
["+6/32", 6 / 32],
// Mixed fractions
["1 3/4", 1.75],
["-1 1/2", -1.5],
// Basic expressions
["6+1/2", 6.5],
["4.5-4.5", 0],
["4.0/2-2+3/5", 3 / 5],
["8 1/2 1/4 1/8 -3/4", 8 + 7 / 8 - 3 / 4],
["8 1/2 + 1/4 +1/8 - 3/4", 8 + 7 / 8 - 3 / 4],
// Invalid input
["", NaN],
["abc", NaN],
["12abc", NaN],
["abc123", NaN],
[" 123", NaN],
["123 ", NaN],
["1/2.3", NaN],
]
test.for(cases)("parses %j as %d", ([s, n]) => {
expect(parseFrac(s)).toEqual(n)
})
})

describe('parseFrac(frac(n)) round-trips within 1/32"', () => {
test.for([1, 1.5, 23 / 32, 9000, -128 + 3 / 32])("%d", (n) => {
const s = frac(n)
expect(parseFrac(s)).toEqual(n)
})
})
Loading

0 comments on commit 2533b19

Please sign in to comment.