|
|
|
|
@ -43,7 +43,7 @@ function escapeRegex(s: string): string {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
import { numberFieldMorfo } from '../../../morfo/components/number-field';
|
|
|
|
|
import type { SomaRuntimePart } from '../../runtime.svelte';
|
|
|
|
|
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
|
@ -81,6 +81,8 @@ interface NumberFieldOpts
|
|
|
|
|
export class NumberFieldProvider {
|
|
|
|
|
readonly opts: NumberFieldOpts;
|
|
|
|
|
readonly runtimePart: SomaRuntimePart;
|
|
|
|
|
readonly soma: Soma;
|
|
|
|
|
readonly runtime: SomaRuntime;
|
|
|
|
|
static readonly ctx = context<NumberFieldProvider>('NumberField');
|
|
|
|
|
static get(): NumberFieldProvider | undefined {
|
|
|
|
|
return this.ctx.getOr(undefined) as NumberFieldProvider | undefined;
|
|
|
|
|
@ -93,7 +95,6 @@ export class NumberFieldProvider {
|
|
|
|
|
return new NumberFieldProvider(opts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
readonly soma = Soma.get();
|
|
|
|
|
/**
|
|
|
|
|
* Optional parent Field context. When present, NumberField inherits
|
|
|
|
|
* `disabled`/`readonly`/`required`/`invalid` and its Input registers into
|
|
|
|
|
@ -116,20 +117,18 @@ export class NumberFieldProvider {
|
|
|
|
|
|
|
|
|
|
private constructor(opts: NumberFieldOpts) {
|
|
|
|
|
this.opts = opts;
|
|
|
|
|
this.runtimePart = Soma.require()
|
|
|
|
|
.runtime(numberFieldMorfo, {})
|
|
|
|
|
.part('provider', {
|
|
|
|
|
this.soma = Soma.require();
|
|
|
|
|
this.runtime = this.soma.runtime(numberFieldMorfo, {});
|
|
|
|
|
this.runtimePart = this.runtime.part('provider', {
|
|
|
|
|
id: opts.id,
|
|
|
|
|
ref: opts.ref,
|
|
|
|
|
owner: this,
|
|
|
|
|
context: NumberFieldProvider.ctx
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.locale = readableActive(() => opts.locale.current ?? this.soma?.langs.getLocale() ?? 'en');
|
|
|
|
|
this.locale = readableActive(() => opts.locale.current ?? this.soma.langs.getLocale() ?? 'en');
|
|
|
|
|
|
|
|
|
|
this.resolvedDir = readableActive(
|
|
|
|
|
() => opts.dir.current ?? this.soma?.prefs.getDir() ?? 'ltr'
|
|
|
|
|
);
|
|
|
|
|
this.resolvedDir = readableActive(() => opts.dir.current ?? this.soma.prefs.getDir() ?? 'ltr');
|
|
|
|
|
|
|
|
|
|
// Sync inputValue with external value/locale changes.
|
|
|
|
|
// When focused (user is typing), only update if the value itself changed — not on locale/format changes.
|
|
|
|
|
@ -179,12 +178,12 @@ export class NumberFieldProvider {
|
|
|
|
|
|
|
|
|
|
/** Decimal separator for the active locale (locale-aware, override-aware). */
|
|
|
|
|
readonly decimalSeparator = $derived.by(
|
|
|
|
|
() => this.soma?.nums?.getDecimalSeparator() ?? resolveDecimalSeparator(this.locale.current)
|
|
|
|
|
() => this.soma.nums?.getDecimalSeparator() ?? resolveDecimalSeparator(this.locale.current)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/** Group (thousands) separator for the active locale. */
|
|
|
|
|
readonly groupSeparator = $derived.by(
|
|
|
|
|
() => this.soma?.nums?.getGroupSeparator() ?? resolveGroupSeparator(this.locale.current)
|
|
|
|
|
() => this.soma.nums?.getGroupSeparator() ?? resolveGroupSeparator(this.locale.current)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/** Regex of characters accepted during typing. Rebuilt on locale/format change. */
|
|
|
|
|
@ -223,7 +222,7 @@ export class NumberFieldProvider {
|
|
|
|
|
formatValue(value: number | undefined): string {
|
|
|
|
|
if (value === undefined || Number.isNaN(value)) return '';
|
|
|
|
|
const opts = this.opts.formatOptions.current;
|
|
|
|
|
const nums = this.soma?.nums;
|
|
|
|
|
const nums = this.soma.nums;
|
|
|
|
|
if (nums) return nums.format(value, opts);
|
|
|
|
|
return intlFormat(this.locale.current, opts).format(value);
|
|
|
|
|
}
|
|
|
|
|
@ -233,7 +232,7 @@ export class NumberFieldProvider {
|
|
|
|
|
if (value === undefined || Number.isNaN(value)) return '';
|
|
|
|
|
const scaled = this.toEditValue(value);
|
|
|
|
|
const opts = this.editFormatOptions;
|
|
|
|
|
const nums = this.soma?.nums;
|
|
|
|
|
const nums = this.soma.nums;
|
|
|
|
|
if (nums) return nums.format(scaled, opts);
|
|
|
|
|
return intlFormat(this.locale.current, opts).format(scaled);
|
|
|
|
|
}
|
|
|
|
|
@ -241,7 +240,7 @@ export class NumberFieldProvider {
|
|
|
|
|
/** Parse edit string and convert to internal value. Returns undefined on failure. */
|
|
|
|
|
parseValue(raw: string): number | undefined {
|
|
|
|
|
if (!raw.trim()) return undefined;
|
|
|
|
|
const nums = this.soma?.nums;
|
|
|
|
|
const nums = this.soma.nums;
|
|
|
|
|
let n: number | undefined;
|
|
|
|
|
if (nums) {
|
|
|
|
|
n = nums.parse(raw);
|
|
|
|
|
@ -451,10 +450,12 @@ export class NumberFieldInputProvider {
|
|
|
|
|
|
|
|
|
|
private constructor(opts: NumberFieldInputOpts) {
|
|
|
|
|
this.opts = opts;
|
|
|
|
|
this.runtimePart = Soma.require()
|
|
|
|
|
.runtime(numberFieldMorfo, {})
|
|
|
|
|
.part('input', { id: opts.id, ref: opts.ref, owner: this });
|
|
|
|
|
this.provider = NumberFieldProvider.require();
|
|
|
|
|
this.runtimePart = this.provider.runtime.part('input', {
|
|
|
|
|
id: opts.id,
|
|
|
|
|
ref: opts.ref,
|
|
|
|
|
owner: this
|
|
|
|
|
});
|
|
|
|
|
// When embedded inside Field, register this input id so Field.Label's
|
|
|
|
|
// `for=` attribute targets it.
|
|
|
|
|
if (this.provider.field) {
|
|
|
|
|
@ -525,10 +526,12 @@ export class NumberFieldIncrementTriggerProvider {
|
|
|
|
|
|
|
|
|
|
private constructor(opts: NumberFieldIncrementTriggerOpts) {
|
|
|
|
|
this.opts = opts;
|
|
|
|
|
this.runtimePart = Soma.require()
|
|
|
|
|
.runtime(numberFieldMorfo, {})
|
|
|
|
|
.part('increment-trigger', { id: opts.id, ref: opts.ref, owner: this });
|
|
|
|
|
this.provider = NumberFieldProvider.require();
|
|
|
|
|
this.runtimePart = this.provider.runtime.part('increment-trigger', {
|
|
|
|
|
id: opts.id,
|
|
|
|
|
ref: opts.ref,
|
|
|
|
|
owner: this
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
readonly isDisabled = $derived.by(() => this.provider.isDisabled || this.provider.atMax);
|
|
|
|
|
@ -543,7 +546,7 @@ export class NumberFieldIncrementTriggerProvider {
|
|
|
|
|
type: 'button' as const,
|
|
|
|
|
tabindex: -1,
|
|
|
|
|
disabled: this.isDisabled || undefined,
|
|
|
|
|
'aria-label': this.provider.soma?.langs.ts(NUMBER_FIELD_LANGS.INCREMENT),
|
|
|
|
|
'aria-label': this.provider.soma.langs.ts(NUMBER_FIELD_LANGS.INCREMENT),
|
|
|
|
|
'aria-disabled': boolToStr(this.isDisabled),
|
|
|
|
|
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
|
|
|
|
|
onclick: this.handleClick
|
|
|
|
|
@ -566,10 +569,12 @@ export class NumberFieldDecrementTriggerProvider {
|
|
|
|
|
|
|
|
|
|
private constructor(opts: NumberFieldDecrementTriggerOpts) {
|
|
|
|
|
this.opts = opts;
|
|
|
|
|
this.runtimePart = Soma.require()
|
|
|
|
|
.runtime(numberFieldMorfo, {})
|
|
|
|
|
.part('decrement-trigger', { id: opts.id, ref: opts.ref, owner: this });
|
|
|
|
|
this.provider = NumberFieldProvider.require();
|
|
|
|
|
this.runtimePart = this.provider.runtime.part('decrement-trigger', {
|
|
|
|
|
id: opts.id,
|
|
|
|
|
ref: opts.ref,
|
|
|
|
|
owner: this
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
readonly isDisabled = $derived.by(() => this.provider.isDisabled || this.provider.atMin);
|
|
|
|
|
@ -584,7 +589,7 @@ export class NumberFieldDecrementTriggerProvider {
|
|
|
|
|
type: 'button' as const,
|
|
|
|
|
tabindex: -1,
|
|
|
|
|
disabled: this.isDisabled || undefined,
|
|
|
|
|
'aria-label': this.provider.soma?.langs.ts(NUMBER_FIELD_LANGS.DECREMENT),
|
|
|
|
|
'aria-label': this.provider.soma.langs.ts(NUMBER_FIELD_LANGS.DECREMENT),
|
|
|
|
|
'aria-disabled': boolToStr(this.isDisabled),
|
|
|
|
|
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
|
|
|
|
|
onclick: this.handleClick
|
|
|
|
|
@ -619,10 +624,12 @@ export class NumberFieldScrubberProvider {
|
|
|
|
|
|
|
|
|
|
private constructor(opts: NumberFieldScrubberOpts) {
|
|
|
|
|
this.opts = opts;
|
|
|
|
|
this.runtimePart = Soma.require()
|
|
|
|
|
.runtime(numberFieldMorfo, {})
|
|
|
|
|
.part('scrubber', { id: opts.id, ref: opts.ref, owner: this });
|
|
|
|
|
this.provider = NumberFieldProvider.require();
|
|
|
|
|
this.runtimePart = this.provider.runtime.part('scrubber', {
|
|
|
|
|
id: opts.id,
|
|
|
|
|
ref: opts.ref,
|
|
|
|
|
owner: this
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private get isDisabled(): boolean {
|
|
|
|
|
|