Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion v2/pink-sb/src/lib/input/Nullable.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import Checkbox from '$lib/selector/Checkbox.svelte';

export let value: string | number | boolean | undefined | null;
export let value: string | number | boolean | bigint | undefined | null;

$: checked = value === null || value === undefined || value === '';
$: hasValidContent = value !== null && value !== undefined && value !== '';
Expand Down
147 changes: 131 additions & 16 deletions v2/pink-sb/src/lib/input/Number.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,138 @@
import Icon from '$lib/Icon.svelte';
import { autofocusInput } from './autofocus.js';
import { IconChevronUp, IconChevronDown } from '@appwrite.io/pink-icons-svelte';
import { parseBigIntBound, parseBigIntStep, parseBigIntValue } from './bigint.js';
import type { HTMLInputAttributes } from 'svelte/elements';
import type { States } from './types.js';
import { createEventDispatcher, type ComponentType } from 'svelte';

type $$Props = Omit<HTMLInputAttributes, 'type'> &
type NativeNumber = HTMLInputAttributes['value'];
type ExtendedNumber = NativeNumber | bigint;

type $$Props = Omit<HTMLInputAttributes, 'type' | 'min' | 'max' | 'value'> &
Partial<{
label: string;
state: States;
helper: string;
nullable: boolean;
autofocus: boolean;
leadingIcon?: ComponentType;
min: ExtendedNumber;
max: ExtendedNumber;
value: ExtendedNumber;
}>;

export let state: States = 'default';
export let nullable: $$Props['nullable'] = false;
export let disabled: $$Props['disabled'] = false;
export let id: $$Props['id'] = undefined;
export let value: $$Props['value'] = undefined;
export let label: $$Props['label'] = undefined;
export let helper: $$Props['helper'] = undefined;
export let readonly: $$Props['readonly'] = false;
export let required: $$Props['required'] = false;
export let autofocus: $$Props['autofocus'] = false;
export let leadingIcon: $$Props['leadingIcon'] = undefined;
export let step: $$Props['step'] = undefined;

export let min: ExtendedNumber = undefined;
export let max: ExtendedNumber = undefined;
export let value: ExtendedNumber = undefined;

let bigintMode = false;
let minAttr: NativeNumber = undefined;
let maxAttr: NativeNumber = undefined;

let input: HTMLInputElement;
const dispatch = createEventDispatcher();

$: if (typeof value === 'bigint' || typeof value === 'number') {
bigintMode = typeof value === 'bigint';
}

function toNumberInputBound(raw: ExtendedNumber): NativeNumber {
if (raw === null || raw === undefined) {
return raw;
}

if (typeof raw === 'bigint') {
const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);

if (raw > maxSafe) {
return Number.MAX_SAFE_INTEGER;
}

const minSafe = BigInt(Number.MIN_SAFE_INTEGER);

if (raw < minSafe) {
return Number.MIN_SAFE_INTEGER;
}

return Number(raw);
}

return raw;
}

$: minAttr = bigintMode ? undefined : toNumberInputBound(min);
$: maxAttr = bigintMode ? undefined : toNumberInputBound(max);

function fireOnChangeDispatch() {
if (bigintMode) {
const parsed = parseBigIntValue(value);
dispatch('change', parsed ?? undefined);
return;
}

dispatch('change', Number(value));
}

function increment(): void {
if (bigintMode) {
const stepValue = parseBigIntStep(step);
const current = parseBigIntValue(value) ?? 0n;
const minValue = parseBigIntBound(min);
const maxValue = parseBigIntBound(max);
let next = current + stepValue;

if (minValue !== null && next < minValue) {
next = minValue;
}

if (maxValue !== null && next > maxValue) {
next = maxValue;
}

value = next.toString();
fireOnChangeDispatch();
return;
}

input.stepUp();
value = input.value;
fireOnChangeDispatch();
}

function decrement(): void {
if (bigintMode) {
const stepValue = parseBigIntStep(step);
const current = parseBigIntValue(value) ?? 0n;
const minValue = parseBigIntBound(min);
const maxValue = parseBigIntBound(max);
let next = current - stepValue;

if (minValue !== null && next < minValue) {
next = minValue;
}

if (maxValue !== null && next > maxValue) {
next = maxValue;
}

value = next.toString();
fireOnChangeDispatch();
return;
}

input.stepDown();
value = input.value;
fireOnChangeDispatch();
Expand All @@ -61,20 +153,43 @@
class:error={state === 'error'}
>
<slot name="start" />
<input
{id}
on:input
on:invalid
on:change={fireOnChangeDispatch}
bind:this={input}
bind:value
type="number"
{disabled}
{readonly}
{required}
{...$$restProps}
use:autofocusInput={autofocus}
/>
{#key bigintMode}
{#if bigintMode}
<input
{id}
on:input
on:invalid
on:change={fireOnChangeDispatch}
bind:this={input}
bind:value
type="text"
inputmode="numeric"
{disabled}
{readonly}
{required}
{...$$restProps}
use:autofocusInput={autofocus}
/>
{:else}
<input
{id}
on:input
on:invalid
on:change={fireOnChangeDispatch}
bind:this={input}
bind:value
type="number"
{disabled}
{readonly}
{required}
min={minAttr}
max={maxAttr}
{step}
{...$$restProps}
use:autofocusInput={autofocus}
/>
{/if}
{/key}
{#if nullable}
<Nullable bind:value />
{/if}
Expand Down
86 changes: 86 additions & 0 deletions v2/pink-sb/src/lib/input/bigint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const bigintPattern = /^-?\d+$/;
const defaultStep = 1n;

function parseBigIntString(raw: string): bigint | null {
const trimmed = raw.trim();

if (trimmed === '' || !bigintPattern.test(trimmed)) {
return null;
}

try {
return BigInt(trimmed);
} catch {
return null;
}
}

export function parseBigIntValue(raw: unknown): bigint | null {
if (raw === undefined) {
return null;
}

if (typeof raw === 'bigint') {
return raw;
}

if (typeof raw === 'number') {
return Number.isFinite(raw) ? BigInt(Math.trunc(raw)) : null;
}

const trimmed = String(raw ?? '').trim();

if (trimmed === '') {
return 0n;
}

return parseBigIntString(trimmed);
}

export function parseBigIntBound(raw: unknown): bigint | null {
if (raw === undefined || raw === null) {
return null;
}

if (typeof raw === 'bigint') {
return raw;
}

if (typeof raw === 'number') {
return Number.isFinite(raw) ? BigInt(Math.trunc(raw)) : null;
}

const trimmed = String(raw).trim();

if (trimmed === '') {
return null;
}

return parseBigIntString(trimmed);
}

export function parseBigIntStep(raw: unknown): bigint {
if (typeof raw === 'bigint') {
return raw === 0n ? defaultStep : raw;
}

if (typeof raw === 'number') {
if (!Number.isFinite(raw) || raw === 0) {
return defaultStep;
}

return BigInt(Math.trunc(raw));
}

if (typeof raw === 'string') {
const trimmed = raw.trim();

if (trimmed === '' || trimmed === 'any') {
return defaultStep;
}

return parseBigIntString(trimmed) ?? defaultStep;
}

return defaultStep;
}
10 changes: 10 additions & 0 deletions v2/pink-sb/src/stories/input/Number.stories.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,13 @@
<Story name="Readonly" args={{ readonly: true, value: 42 }} />
<Story name="Disabled" args={{ disabled: true }} />
<Story name="Nullable" args={{ nullable: true }} />
<Story name="BigInt">
<Input.Number
label="BigInt"
helper="Min: 1, Max: 9007199254741001"
value={9007199254740993n}
step="2"
min={1}
max={9007199254741001n}
/>
</Story>