{"version":3,"file":"text-field.cjs","names":[],"sources":["../../src/core/text-field.ts"],"sourcesContent":["import { computed, type Readable, type Signal } from '@vielzeug/ripple';\n\nimport { type CounterState, createCounterState, createField, type FieldHandle, type FieldOptions } from './field-base';\nimport { syncedSignal } from './signals';\n\n// ── Text field ────────────────────────────────────────────────────────────────\n\n/** Detach function returned by `wire()`. Call to remove element listeners. */\nexport type TextFieldDetach = () => void;\n\nexport type TextFieldOptions = FieldOptions & {\n  maxLength?: Readable<number | undefined>;\n  /**\n   * Called synchronously before value extraction on every input event.\n   * Use only for DOM side-effects (e.g. auto-resize measurement) that must\n   * run before the reactive value is updated.\n   */\n  onBeforeInput?: (event: Event) => void;\n  onBlur?: (event: FocusEvent) => void;\n  onChange?: (event: Event, value: string) => void;\n  onFocus?: (event: FocusEvent) => void;\n  onInput?: (event: Event, value: string) => void;\n  /**\n   * Bars the field from constraint validation while true — matches the native HTML rule that a\n   * `readonly` field is never a candidate for constraint validation (a `readonly required`\n   * field is always valid, per spec), so `validity`/`validationMessage` don't feed a false\n   * `valueMissing` into `useField()` just because a read-only field happens to be blank.\n   */\n  readonly?: Readable<boolean | undefined>;\n  /** Marks a blank value invalid — feeds `validity`/`validationMessage` (see below). */\n  required?: Readable<boolean | undefined>;\n  /** Message for the blank+required case. Defaults to `'This field is required.'`. */\n  requiredMessage?: Readable<string | undefined>;\n  /** `AbortSignal` from the component lifecycle. The internal value-sync watcher is disposed on abort. */\n  signal: AbortSignal;\n  value: Readable<string | undefined>;\n};\n\nexport type TextFieldHandle = FieldHandle & {\n  /**\n   * Registers the real form field handle (the return value of `useField()`) so that\n   * `triggerValidation()` — called internally on blur/change — can call its\n   * `reportValidity()`. `useField()` itself needs `value` (below) to exist first, so this\n   * is a two-step wiring rather than a constructor option:\n   *\n   * ```ts\n   * const tf = createTextField({ value: props.value, ... });\n   * const formField = useField({ value: tf.value, ... });\n   * tf.attachFormField(formField);\n   * ```\n   */\n  attachFormField: (formField: { reportValidity(): void }) => void;\n  /** Clears the field value and fires synthetic input/change events. */\n  clear: (event?: Event) => void;\n  /**\n   * Reactive counter state. Non-null when `maxLength` was provided; `null` otherwise.\n   * Components should only render a counter element when this is non-null.\n   */\n  counter: Readable<CounterState> | null;\n  /**\n   * Restores the value to whatever the `value` option currently holds (native form\n   * `reset()` semantics: a `<input>` reverts to its *current* `value` content attribute,\n   * not a frozen snapshot from element creation — so setting the attribute programmatically\n   * after mount changes what a later reset reverts to). Wire into `useField({ onReset: tf.reset })`.\n   */\n  reset: () => void;\n  /** Reactive validation message paired with `validity`. Empty string when valid. */\n  validationMessage: Readable<string>;\n  /**\n   * Reactive `ValidityStateFlags` — `{ valueMissing: true }` while `required` and blank,\n   * `null` (valid) otherwise. Pass straight to `useField({ validity: tf.validity })`.\n   */\n  validity: Readable<ValidityStateFlags | null>;\n  /** The local mutable field value (two-way bound to the input element via `wire()`). */\n  value: Signal<string>;\n  /**\n   * Attaches event listeners to the underlying input or textarea element.\n   *\n   * When `signal` is provided, the listeners are detached automatically when\n   * the signal's controller calls `abort()`. Without a signal the returned\n   * `TextFieldDetach` must be called manually.\n   *\n   * Safe to call multiple times — each call attaches a fresh set of listeners\n   * and returns a distinct detach function. Calling detach multiple times is\n   * a no-op (guarded internally).\n   */\n  wire: (el: HTMLInputElement | HTMLTextAreaElement, signal?: AbortSignal) => TextFieldDetach;\n};\n\nconst isBlank = (value: string): boolean => value.trim() === '';\nconst toFieldValue = (v: string | undefined): string => String(v ?? '');\n\nexport const createTextField = (options: TextFieldOptions): TextFieldHandle => {\n  const value = syncedSignal(options.value, options.signal, toFieldValue);\n\n  const field = createField(options);\n  const counter = options.maxLength ? createCounterState({ maxLength: options.maxLength, value }) : null;\n\n  const validity = computed<ValidityStateFlags | null>(() =>\n    !options.readonly?.value && options.required?.value && isBlank(value.value) ? { valueMissing: true } : null,\n  );\n  const validationMessage = computed(() =>\n    validity.value ? options.requiredMessage?.value || 'This field is required.' : '',\n  );\n\n  const reset = (): void => {\n    value.value = toFieldValue(options.value.value);\n    field.triggerValidation('change');\n  };\n\n  const clear = (event?: Event): void => {\n    event?.preventDefault?.();\n    value.value = '';\n    options.onInput?.(event ?? new InputEvent('input', { bubbles: true }), '');\n    options.onChange?.(event ?? new Event('change', { bubbles: true }), '');\n    field.triggerValidation('change');\n  };\n\n  const wire = (element: HTMLInputElement | HTMLTextAreaElement, signal?: AbortSignal): TextFieldDetach => {\n    const cleanups: Array<() => void> = [];\n    const on = (type: string, handler: EventListener): void => {\n      element.addEventListener(type, handler);\n      cleanups.push(() => element.removeEventListener(type, handler));\n    };\n\n    if (options.onFocus) {\n      on('focus', (e) => options.onFocus?.(e as FocusEvent));\n    }\n\n    on('input', (e) => {\n      e.stopPropagation();\n      options.onBeforeInput?.(e);\n      value.value = element.value;\n      options.onInput?.(e, element.value);\n    });\n\n    on('change', (e) => {\n      e.stopPropagation();\n      value.value = element.value;\n      options.onChange?.(e, element.value);\n      field.triggerValidation('change');\n    });\n\n    on('blur', (e) => {\n      options.onBlur?.(e as FocusEvent);\n      field.triggerValidation('blur');\n    });\n\n    let detached = false;\n    const detach = (): void => {\n      if (detached) return;\n\n      detached = true;\n      for (const dispose of cleanups) dispose();\n    };\n\n    signal?.addEventListener('abort', detach, { once: true });\n\n    return detach;\n  };\n\n  return { ...field, clear, counter, reset, validationMessage, validity, value, wire };\n};\n"],"mappings":"iGAyFA,IAAM,EAAW,GAA2B,EAAM,KAAK,IAAM,GACvD,EAAgB,GAAkC,OAAO,GAAK,EAAE,EAEzD,EAAmB,GAA+C,CAC7E,IAAM,EAAQ,EAAA,aAAa,EAAQ,MAAO,EAAQ,OAAQ,CAAY,EAEhE,EAAQ,EAAA,YAAY,CAAO,EAC3B,EAAU,EAAQ,UAAY,EAAA,mBAAmB,CAAE,UAAW,EAAQ,UAAW,OAAM,CAAC,EAAI,KAE5F,GAAA,EAAW,EAAA,SAAA,KACf,CAAC,EAAQ,UAAU,OAAS,EAAQ,UAAU,OAAS,EAAQ,EAAM,KAAK,EAAI,CAAE,aAAc,EAAK,EAAI,IACzG,EACM,GAAA,EAAoB,EAAA,SAAA,KACxB,EAAS,MAAQ,EAAQ,iBAAiB,OAAS,0BAA4B,EACjF,EAEM,MAAoB,CACxB,EAAM,MAAQ,EAAa,EAAQ,MAAM,KAAK,EAC9C,EAAM,kBAAkB,QAAQ,CAClC,EAEM,EAAS,GAAwB,CACrC,GAAO,iBAAiB,EACxB,EAAM,MAAQ,GACd,EAAQ,UAAU,GAAS,IAAI,WAAW,QAAS,CAAE,QAAS,EAAK,CAAC,EAAG,EAAE,EACzE,EAAQ,WAAW,GAAS,IAAI,MAAM,SAAU,CAAE,QAAS,EAAK,CAAC,EAAG,EAAE,EACtE,EAAM,kBAAkB,QAAQ,CAClC,EAEM,GAAQ,EAAiD,IAA0C,CACvG,IAAM,EAA8B,CAAC,EAC/B,GAAM,EAAc,IAAiC,CACzD,EAAQ,iBAAiB,EAAM,CAAO,EACtC,EAAS,SAAW,EAAQ,oBAAoB,EAAM,CAAO,CAAC,CAChE,EAEI,EAAQ,SACV,EAAG,QAAU,GAAM,EAAQ,UAAU,CAAe,CAAC,EAGvD,EAAG,QAAU,GAAM,CACjB,EAAE,gBAAgB,EAClB,EAAQ,gBAAgB,CAAC,EACzB,EAAM,MAAQ,EAAQ,MACtB,EAAQ,UAAU,EAAG,EAAQ,KAAK,CACpC,CAAC,EAED,EAAG,SAAW,GAAM,CAClB,EAAE,gBAAgB,EAClB,EAAM,MAAQ,EAAQ,MACtB,EAAQ,WAAW,EAAG,EAAQ,KAAK,EACnC,EAAM,kBAAkB,QAAQ,CAClC,CAAC,EAED,EAAG,OAAS,GAAM,CAChB,EAAQ,SAAS,CAAe,EAChC,EAAM,kBAAkB,MAAM,CAChC,CAAC,EAED,IAAI,EAAW,GACT,MAAqB,CACrB,MAEJ,GAAW,GACX,IAAK,IAAM,KAAW,EAAU,EAAQ,CAD7B,CAEb,EAIA,OAFA,GAAQ,iBAAiB,QAAS,EAAQ,CAAE,KAAM,EAAK,CAAC,EAEjD,CACT,EAEA,MAAO,CAAE,GAAG,EAAO,QAAO,UAAS,QAAO,oBAAmB,WAAU,QAAO,MAAK,CACrF"}