{"version":3,"file":"choice-field.cjs","names":[],"sources":["../../src/core/choice-field.ts"],"sourcesContent":["import { computed, type Readable, watch } from '@vielzeug/ripple';\n\nimport { createDirtyTracker, createField, type FieldHandle, type FieldOptions } from './field-base';\nimport { syncedSignal } from './signals';\n\n// ── Choice change event payload ──────────────────────────────────────────────────────────────\n\nexport type ChoiceChangeDetail = {\n  labels: string[];\n  originalEvent?: Event;\n  values: string[];\n};\n\n// ── Choice field ──────────────────────────────────────────────────────────────────────\n\nexport type ChoiceFieldOptions = FieldOptions & {\n  multiple?: Readable<boolean | undefined>;\n  /** Marks an empty selection invalid — feeds `validity`/`validationMessage` (see below). */\n  required?: Readable<boolean | undefined>;\n  /** Message for the empty-selection+required case. Defaults to `'Please make a selection.'`. */\n  requiredMessage?: Readable<string | undefined>;\n  /** `AbortSignal` from the component lifecycle. All internal subscriptions are disposed on abort. */\n  signal: AbortSignal;\n  /**\n   * Current selection value(s). Accepts a comma-separated string\n   * (legacy/single-select form serialisation) or a `string[]` array.\n   * The field normalises both formats internally via `parseChoiceValues`.\n   */\n  value: Readable<string | string[] | undefined>;\n};\n\nexport type ChoiceFieldHandle = FieldHandle & {\n  clear: () => void;\n  formValue: Readable<string>;\n  removeValue: (value: string) => void;\n  /**\n   * Restores the selection to its native \"default\". See `DirtyTracker` for the two-state\n   * rationale.\n   */\n  reset: () => void;\n  selectedValue: Readable<string | undefined>;\n  selectedValues: Readable<string[]>;\n  selectValue: (value: string) => void;\n  setValues: (values: string[]) => void;\n  /**\n   * Toggles `value` in the selection.\n   * - **Multiple mode**: adds if absent, removes if present.\n   * - **Single mode**: always selects `value` (acts like `selectValue` — cannot\n   *   deselect; use `clear()` to reset a single-select field).\n   */\n  toggleValue: (value: string) => 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 no option is\n   * selected, `null` (valid) otherwise. Pass straight to `useField({ validity: choice.validity })`.\n   */\n  validity: Readable<ValidityStateFlags | null>;\n};\n\nconst parseChoiceValues = (raw: string | string[] | undefined): string[] => {\n  if (!raw) return [];\n\n  if (Array.isArray(raw)) return raw.filter(Boolean);\n\n  return raw\n    .split(',')\n    .map((entry) => entry.trim())\n    .filter(Boolean);\n};\n\nexport const createChoiceField = (options: ChoiceFieldOptions): ChoiceFieldHandle => {\n  const isMultiple = computed(() => Boolean(options.multiple?.value));\n\n  const normalizeValues = (values: string[]): string[] => {\n    const normalized = isMultiple.value ? values : values.slice(0, 1);\n\n    return [...new Set(normalized.filter(Boolean))];\n  };\n\n  // Kept in sync with `options.value` via the shared helper (see `checkable.ts`/`text-field.ts`\n  // for the same pattern) rather than a hand-rolled signal + watch pair.\n  const selectedValues = syncedSignal(options.value, options.signal, (raw) => normalizeValues(parseChoiceValues(raw)));\n  const formValue = computed(() =>\n    isMultiple.value ? selectedValues.value.join(',') : (selectedValues.value[0] ?? ''),\n  );\n\n  const dirtyTracker = createDirtyTracker();\n\n  // The actual state setter, shared by every mutation path. Deliberately *not* where the dirty\n  // tracker gets marked — `syncFromProp` (below) calls this directly to stay exempt from it,\n  // since mirroring an external prop change is never \"the user changed the selection\".\n  const applyValues = (values: string[]): void => {\n    selectedValues.value = normalizeValues(values);\n  };\n\n  const setValues = (values: string[]): void => {\n    dirtyTracker.markDirty();\n    applyValues(values);\n  };\n  const clear = (): void => setValues([]);\n  const removeValue = (value: string): void => {\n    dirtyTracker.markDirty();\n    selectedValues.value = selectedValues.value.filter((v) => v !== value);\n  };\n\n  const selectValue = (value: string): void => {\n    if (isMultiple.value) {\n      if (!selectedValues.value.includes(value)) setValues([...selectedValues.value, value]);\n\n      return;\n    }\n\n    setValues([value]);\n  };\n\n  const toggleValue = (value: string): void => {\n    if (isMultiple.value) {\n      if (selectedValues.value.includes(value)) removeValue(value);\n      else selectValue(value);\n    } else {\n      selectValue(value);\n    }\n  };\n\n  const syncFromProp = (nextValue: string | string[] | undefined): void => {\n    applyValues(parseChoiceValues(nextValue));\n  };\n\n  const field = createField(options);\n\n  const validity = computed<ValidityStateFlags | null>(() =>\n    options.required?.value && selectedValues.value.length === 0 ? { valueMissing: true } : null,\n  );\n  const validationMessage = computed(() =>\n    validity.value ? options.requiredMessage?.value || 'Please make a selection.' : '',\n  );\n\n  if (options.multiple) {\n    const multipleSub = watch(options.multiple, () => syncFromProp(options.value.value));\n\n    options.signal.addEventListener('abort', () => multipleSub.dispose(), { once: true });\n  }\n\n  const initialValues = selectedValues.value;\n  const reset = (): void => {\n    applyValues(dirtyTracker.isDirty ? initialValues : parseChoiceValues(options.value.value));\n    dirtyTracker.clear();\n    field.triggerValidation('change');\n  };\n\n  return {\n    ...field,\n    clear,\n    formValue,\n    removeValue,\n    reset,\n    selectedValue: computed(() => selectedValues.value[0]),\n    selectedValues,\n    selectValue,\n    setValues,\n    toggleValue,\n    validationMessage,\n    validity,\n  };\n};\n"],"mappings":"iGA4DA,IAAM,EAAqB,GACpB,EAED,MAAM,QAAQ,CAAG,EAAU,EAAI,OAAO,OAAO,EAE1C,EACJ,MAAM,GAAG,CAAC,CACV,IAAK,GAAU,EAAM,KAAK,CAAC,CAAC,CAC5B,OAAO,OAAO,EAPA,CAAC,EAUP,EAAqB,GAAmD,CACnF,IAAM,GAAA,EAAa,EAAA,SAAA,KAAe,EAAQ,EAAQ,UAAU,KAAM,EAE5D,EAAmB,GAA+B,CACtD,IAAM,EAAa,EAAW,MAAQ,EAAS,EAAO,MAAM,EAAG,CAAC,EAEhE,MAAO,CAAC,GAAG,IAAI,IAAI,EAAW,OAAO,OAAO,CAAC,CAAC,CAChD,EAIM,EAAiB,EAAA,aAAa,EAAQ,MAAO,EAAQ,OAAS,GAAQ,EAAgB,EAAkB,CAAG,CAAC,CAAC,EAC7G,GAAA,EAAY,EAAA,SAAA,KAChB,EAAW,MAAQ,EAAe,MAAM,KAAK,GAAG,EAAK,EAAe,MAAM,IAAM,EAClF,EAEM,EAAe,EAAA,mBAAmB,EAKlC,EAAe,GAA2B,CAC9C,EAAe,MAAQ,EAAgB,CAAM,CAC/C,EAEM,EAAa,GAA2B,CAC5C,EAAa,UAAU,EACvB,EAAY,CAAM,CACpB,EACM,MAAoB,EAAU,CAAC,CAAC,EAChC,EAAe,GAAwB,CAC3C,EAAa,UAAU,EACvB,EAAe,MAAQ,EAAe,MAAM,OAAQ,GAAM,IAAM,CAAK,CACvE,EAEM,EAAe,GAAwB,CAC3C,GAAI,EAAW,MAAO,CACf,EAAe,MAAM,SAAS,CAAK,GAAG,EAAU,CAAC,GAAG,EAAe,MAAO,CAAK,CAAC,EAErF,MACF,CAEA,EAAU,CAAC,CAAK,CAAC,CACnB,EAEM,EAAe,GAAwB,CACvC,EAAW,OACT,EAAe,MAAM,SAAS,CAAK,EAAG,EAAY,CAAK,EACtD,EAAY,CAAK,CAI1B,EAEM,EAAgB,GAAmD,CACvE,EAAY,EAAkB,CAAS,CAAC,CAC1C,EAEM,EAAQ,EAAA,YAAY,CAAO,EAE3B,GAAA,EAAW,EAAA,SAAA,KACf,EAAQ,UAAU,OAAS,EAAe,MAAM,SAAW,EAAI,CAAE,aAAc,EAAK,EAAI,IAC1F,EACM,GAAA,EAAoB,EAAA,SAAA,KACxB,EAAS,MAAQ,EAAQ,iBAAiB,OAAS,2BAA6B,EAClF,EAEA,GAAI,EAAQ,SAAU,CACpB,IAAM,GAAA,EAAc,EAAA,MAAA,CAAM,EAAQ,aAAgB,EAAa,EAAQ,MAAM,KAAK,CAAC,EAEnF,EAAQ,OAAO,iBAAiB,YAAe,EAAY,QAAQ,EAAG,CAAE,KAAM,EAAK,CAAC,CACtF,CAEA,IAAM,EAAgB,EAAe,MAC/B,MAAoB,CACxB,EAAY,EAAa,QAAU,EAAgB,EAAkB,EAAQ,MAAM,KAAK,CAAC,EACzF,EAAa,MAAM,EACnB,EAAM,kBAAkB,QAAQ,CAClC,EAEA,MAAO,CACL,GAAG,EACH,QACA,YACA,cACA,QACA,eAAA,EAAe,EAAA,SAAA,KAAe,EAAe,MAAM,EAAE,EACrD,iBACA,cACA,YACA,cACA,oBACA,UACF,CACF"}