|
| 1 | +<script lang="ts"> |
| 2 | + import { range, unique } from './utils.js'; |
| 3 | +
|
| 4 | + interface Props { |
| 5 | + min: number; |
| 6 | + max: number; |
| 7 | + /** Maximum step size */ |
| 8 | + granularity?: number | undefined; |
| 9 | + /** Number of steps to use. If not set, uses as much steps as granularity allows */ |
| 10 | + stepcount?: number; |
| 11 | + /** Array of numbers to specify every tick's value, a single number of specify a number of ticks to uniformly distribute on the slider */ |
| 12 | + ticks?: number | number[]; |
| 13 | + value: number | undefined; |
| 14 | + disabled?: boolean; |
| 15 | + onblur?: () => void; |
| 16 | + // eslint-disable-next-line no-unused-vars |
| 17 | + onvalue?: (value: number) => void; |
| 18 | + } |
| 19 | +
|
| 20 | + let { |
| 21 | + min, |
| 22 | + max, |
| 23 | + stepcount = Infinity, |
| 24 | + granularity, |
| 25 | + ticks = 0, |
| 26 | + value = $bindable(), |
| 27 | + disabled, |
| 28 | + onblur, |
| 29 | + onvalue |
| 30 | + }: Props = $props(); |
| 31 | +
|
| 32 | + const componentId = $props.id(); |
| 33 | +
|
| 34 | + const stepsize = $derived((max - min) / (stepcount - 1)); |
| 35 | + const step = $derived(granularity ? Math.max(stepsize, granularity) : stepsize); |
| 36 | +</script> |
| 37 | + |
| 38 | +<input |
| 39 | + type="range" |
| 40 | + {min} |
| 41 | + {max} |
| 42 | + {step} |
| 43 | + {disabled} |
| 44 | + bind:value |
| 45 | + list="{componentId}-ticks" |
| 46 | + style:--slider-width="{value ? ((value - min) / (max - min)) * 100 : 0}%" |
| 47 | + {onblur} |
| 48 | + oninput={({ currentTarget }) => { |
| 49 | + onvalue?.( |
| 50 | + granularity && granularity >= 1 |
| 51 | + ? Number.parseInt(currentTarget.value, 10) |
| 52 | + : Number.parseFloat(currentTarget.value) |
| 53 | + ); |
| 54 | + }} |
| 55 | +/> |
| 56 | + |
| 57 | +{#if Array.isArray(ticks) || ticks > 0} |
| 58 | + <datalist id="{componentId}-ticks"> |
| 59 | + {#if Array.isArray(ticks)} |
| 60 | + {#each unique(ticks) as tick (tick)} |
| 61 | + <option value={tick}></option> |
| 62 | + {/each} |
| 63 | + {:else} |
| 64 | + {#each range(ticks) as i (i)} |
| 65 | + <option value={min + (i * (max - min)) / (ticks - 1)}></option> |
| 66 | + {/each} |
| 67 | + {/if} |
| 68 | + </datalist> |
| 69 | +{/if} |
| 70 | + |
| 71 | +<style> |
| 72 | + /* Many thanks to CSS Tricks: https://css-tricks.com/sliding-nightmare-understanding-range-input/ */ |
| 73 | +
|
| 74 | + input { |
| 75 | + margin: 0; |
| 76 | + padding: 0; |
| 77 | + width: 100%; |
| 78 | + height: 1.5em; |
| 79 | + background: transparent; |
| 80 | +
|
| 81 | + --track-height: 0.5em; |
| 82 | + --track-color: var(--track-background, var(--bg-primary-translucent)); |
| 83 | + --track-filled-color: var(--track-fill, var(--bg-primary)); |
| 84 | + --thumb-color: var(--track-thumb, var(--bg-neutral)); |
| 85 | +
|
| 86 | + &:is(:hover, :focus-visible) { |
| 87 | + --track-filled-color: var(--track-fill, var(--fg-primary)); |
| 88 | + } |
| 89 | + } |
| 90 | +
|
| 91 | + input, |
| 92 | + input::-webkit-slider-thumb { |
| 93 | + -webkit-appearance: none; |
| 94 | + } |
| 95 | +
|
| 96 | + input::-ms-tooltip { |
| 97 | + display: none; |
| 98 | + } |
| 99 | +
|
| 100 | + /* Full track */ |
| 101 | +
|
| 102 | + input::-webkit-slider-runnable-track { |
| 103 | + box-sizing: border-box; |
| 104 | + border: none; |
| 105 | + width: 100%; |
| 106 | + height: var(--track-height); |
| 107 | + background: var(--track-color); |
| 108 | + border-radius: 9999px; |
| 109 | + } |
| 110 | +
|
| 111 | + input:is(:focus-visible)::-webkit-slider-runnable-track { |
| 112 | + border: 1px solid var(--fg-neutral); |
| 113 | + } |
| 114 | +
|
| 115 | + input::-moz-range-track { |
| 116 | + box-sizing: border-box; |
| 117 | + border: none; |
| 118 | + width: 100%; |
| 119 | + height: var(--track-height); |
| 120 | + background: var(--track-color); |
| 121 | + border-radius: 9999px; |
| 122 | + } |
| 123 | +
|
| 124 | + input:is(:focus-visible)::-moz-range-track { |
| 125 | + border: 1px solid var(--fg-neutral); |
| 126 | + } |
| 127 | +
|
| 128 | + input::-ms-track { |
| 129 | + box-sizing: border-box; |
| 130 | + border: none; |
| 131 | + width: 100%; |
| 132 | + height: var(--track-height); |
| 133 | + background: var(--track-color); |
| 134 | + border-radius: 9999px; |
| 135 | + } |
| 136 | +
|
| 137 | + input:is(:focus-visible)::-ms-track { |
| 138 | + border: 1px solid var(--fg-neutral); |
| 139 | + } |
| 140 | +
|
| 141 | + /* Thumb */ |
| 142 | +
|
| 143 | + input::-webkit-slider-thumb { |
| 144 | + cursor: pointer; |
| 145 | + margin-top: -0.625em; |
| 146 | + box-sizing: border-box; |
| 147 | + border: none; |
| 148 | + width: 1.5em; |
| 149 | + height: 1.5em; |
| 150 | + border-radius: 50%; |
| 151 | + background: var(--thumb-color); |
| 152 | + border: 1px solid var(--fg-neutral); |
| 153 | + transition: border 0.1s ease; |
| 154 | + } |
| 155 | +
|
| 156 | + input:is(:hover, :focus-visible)::-webkit-slider-thumb { |
| 157 | + border-color: var(--track-filled-color); |
| 158 | + } |
| 159 | +
|
| 160 | + input:is(:active)::-webkit-slider-thumb { |
| 161 | + border-width: calc(1.5em / 2 - 5px); |
| 162 | + } |
| 163 | +
|
| 164 | + input::-moz-range-thumb { |
| 165 | + cursor: pointer; |
| 166 | + box-sizing: border-box; |
| 167 | + border: none; |
| 168 | + width: 1.5em; |
| 169 | + height: 1.5em; |
| 170 | + border-radius: 50%; |
| 171 | + background: var(--thumb-color); |
| 172 | + border: 1px solid var(--fg-neutral); |
| 173 | + transition: border 0.1s ease; |
| 174 | + } |
| 175 | +
|
| 176 | + input:is(:hover, :focus-visible)::-moz-range-thumb { |
| 177 | + border-color: var(--track-filled-color); |
| 178 | + } |
| 179 | +
|
| 180 | + input:is(:active)::-moz-range-thumb { |
| 181 | + border-width: calc(1.5em / 2 - 5px); |
| 182 | + } |
| 183 | +
|
| 184 | + input::-ms-thumb { |
| 185 | + cursor: pointer; |
| 186 | + margin-top: 0; |
| 187 | + box-sizing: border-box; |
| 188 | + border: none; |
| 189 | + width: 1.5em; |
| 190 | + height: 1.5em; |
| 191 | + border-radius: 50%; |
| 192 | + background: var(--thumb-color); |
| 193 | + border: 1px solid var(--fg-neutral); |
| 194 | + transition: border 0.1s ease; |
| 195 | + } |
| 196 | +
|
| 197 | + input:is(:hover, :focus-visible)::-ms-thumb { |
| 198 | + border-color: var(--track-filled-color); |
| 199 | + } |
| 200 | +
|
| 201 | + input:is(:active)::-ms-thumb { |
| 202 | + border-width: calc(1.5em / 2 - 5px); |
| 203 | + } |
| 204 | +
|
| 205 | + /* Filled track */ |
| 206 | +
|
| 207 | + input::-webkit-slider-runnable-track { |
| 208 | + transition: background 0.1s ease; |
| 209 | + background: linear-gradient( |
| 210 | + to right, |
| 211 | + var(--track-filled-color) var(--slider-width), |
| 212 | + var(--track-color) var(--slider-width) |
| 213 | + ); |
| 214 | + } |
| 215 | +
|
| 216 | + input::-moz-range-progress { |
| 217 | + transition: background 0.1s ease; |
| 218 | + height: var(--track-height); |
| 219 | + background: var(--track-filled-color); |
| 220 | + border-radius: 9999px; |
| 221 | + } |
| 222 | +
|
| 223 | + input::-ms-fill-lower { |
| 224 | + transition: background 0.1s ease; |
| 225 | + height: var(--track-height); |
| 226 | + background: var(--track-filled-color); |
| 227 | + border-radius: 9999px; |
| 228 | + } |
| 229 | +</style> |
0 commit comments