{"version":3,"file":"host-bind.cjs","names":[],"sources":["../src/host-bind.ts"],"sourcesContent":["/**\n * Host element binding API — reactive attr, class, style, and event bindings\n * applied directly to the component's host element or any target element.\n */\n\nimport { isReactive, type Readable } from '@vielzeug/ripple';\n\nimport { getHost, tryRegisterCleanup, watchEffect } from './runtime';\nimport { normalizeAriaKey, normalizeHostAttrKey } from './utils/aria';\nimport { listen, resolveMaybeReactive, sanitizeCssToken, setAttr, toKebab } from './utils/dom';\n\n/**\n * Describes a reactive or static host binding value.\n */\nexport type HostBindingValue =\n  | (() => string | number | boolean | null | undefined)\n  | Readable<string | number | boolean | null | undefined>\n  | string\n  | number\n  | boolean\n  | null\n  | undefined;\n\n/**\n * Configuration for host attribute bindings.\n */\nexport type ReflectConfig = Record<string, HostBindingValue>;\n\ntype HostClassBindingValue = Readable<boolean> | (() => boolean) | boolean;\n// Bivariant callback allows consumers to use narrower event types.\ntype HostEventListener = { bivarianceHack(event: Event): void }['bivarianceHack'];\n\nexport type HostBindConfig = {\n  /**\n   * ARIA attributes, keyed by bare property name (`expanded`) or fully-qualified\n   * (`aria-expanded`) — both normalize to the same attribute. A separate key from `attr`\n   * only so bare names can be normalized; the underlying write path is identical.\n   */\n  aria?: ReflectConfig;\n  attr?: ReflectConfig;\n  class?: (() => Record<string, boolean>) | Record<string, HostClassBindingValue>;\n  on?: Record<string, HostEventListener | undefined>;\n  style?: Record<string, HostBindingValue>;\n};\n\nexport type BindOptions = AddEventListenerOptions & {\n  /**\n   * Target element to bind to. Defaults to the host element when called\n   * via `bind()`. Pass an explicit element to bind to any other element\n   * (e.g. a slotted trigger or an internally-referenced child element).\n   * When a target is provided, cleanup is always auto-registered with the\n   * component scope if one is active.\n   */\n  target?: Element;\n};\n\nexport type HostBindFn = (config: HostBindConfig, options?: BindOptions) => () => void;\n\n/**\n * Apply reactive or static bindings to an element's attributes, classes, styles,\n * and events. Defaults to the current component's host element; pass\n * `options.target` to bind to any other element (e.g. a slotted trigger, an\n * internally-referenced child).\n */\nexport const bind: HostBindFn = (config: HostBindConfig, options?: BindOptions): (() => void) => {\n  const el = (options?.target as HTMLElement | undefined) ?? getHost();\n  const disposers: Array<() => void> = [];\n\n  if (config.attr) {\n    for (const [key, value] of Object.entries(config.attr)) {\n      const name = toHostAttr(key);\n      const dispose = applyAttribute(el, name, value);\n\n      if (dispose) disposers.push(dispose);\n    }\n  }\n\n  if (config.aria) {\n    for (const [key, value] of Object.entries(config.aria)) {\n      const name = normalizeAriaKey(key);\n      const dispose = applyAttribute(el, name, value);\n\n      if (dispose) disposers.push(dispose);\n    }\n  }\n\n  if (config.class) {\n    disposers.push(applyClassMap(el, config.class));\n  }\n\n  if (config.style) {\n    for (const [key, value] of Object.entries(config.style)) {\n      const dispose = applyStyle(el, key, value);\n\n      if (dispose) disposers.push(dispose);\n    }\n  }\n\n  if (config.on) {\n    const { target: _t, ...listenerOptions } = options ?? {};\n\n    for (const event of Object.keys(config.on) as Array<keyof typeof config.on>) {\n      const listener = config.on[event];\n\n      if (!listener) continue;\n\n      disposers.push(listen(el, event as string, listener as EventListener, listenerOptions));\n    }\n  }\n\n  const cleanup = (): void => {\n    for (const dispose of disposers) dispose();\n  };\n\n  tryRegisterCleanup(cleanup);\n\n  return cleanup;\n};\n\nconst toHostAttr = normalizeHostAttrKey;\n\nconst applyReactiveBinding = (\n  value: HostBindingValue,\n  updater: (next: string | number | boolean | null | undefined) => void,\n): (() => void) | undefined => {\n  if (typeof value === 'function') {\n    return watchEffect(() => {\n      updater(value());\n      return undefined;\n    });\n  }\n\n  if (isReactive(value)) {\n    return watchEffect(() => {\n      updater(value.value);\n      return undefined;\n    });\n  }\n\n  updater(value);\n};\n\nfunction applyAttribute(host: HTMLElement, name: string, value: HostBindingValue): (() => void) | undefined {\n  return applyReactiveBinding(value, (next) => setAttr(host, name, next));\n}\n\nfunction applyStyle(host: HTMLElement, name: string, value: HostBindingValue): (() => void) | undefined {\n  const cssName = sanitizeCssToken(name.startsWith('--') ? name : toKebab(name));\n\n  if (!cssName) return;\n\n  let owned = false;\n  const setStyle = (v: string | number | boolean | null | undefined): void => {\n    if (v != null && v !== '') {\n      owned = true;\n      host.style.setProperty(cssName, sanitizeCssToken(String(v)));\n    } else if (owned) host.style.removeProperty(cssName);\n  };\n\n  return applyReactiveBinding(value, setStyle);\n}\n\nfunction applyClassMap(\n  host: HTMLElement,\n  value: (() => Record<string, boolean>) | Record<string, HostClassBindingValue>,\n): () => void {\n  const getMap =\n    typeof value === 'function'\n      ? value\n      : (): Record<string, boolean> => {\n          const result: Record<string, boolean> = {};\n\n          for (const [cls, entry] of Object.entries(value)) {\n            result[cls] = resolveMaybeReactive(entry);\n          }\n\n          return result;\n        };\n\n  let prev = new Set<string>();\n\n  const sub = watchEffect(() => {\n    const next = new Set<string>();\n\n    for (const [cls, active] of Object.entries(getMap())) {\n      if (!active) continue;\n\n      next.add(cls);\n\n      if (!prev.has(cls)) host.classList.add(cls);\n    }\n    for (const cls of prev) {\n      if (!next.has(cls)) host.classList.remove(cls);\n    }\n    prev = next;\n  });\n\n  return sub;\n}\n"],"mappings":"8HAgEA,IAAa,GAAoB,EAAwB,IAAwC,CAC/F,IAAM,EAAM,GAAS,QAAsC,EAAA,QAAQ,EAC7D,EAA+B,CAAC,EAEtC,GAAI,EAAO,KACT,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,EAAO,IAAI,EAAG,CAEtD,IAAM,EAAU,EAAe,EADlB,EAAW,CACW,EAAM,CAAK,EAE1C,GAAS,EAAU,KAAK,CAAO,CACrC,CAGF,GAAI,EAAO,KACT,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,EAAO,IAAI,EAAG,CAEtD,IAAM,EAAU,EAAe,EADlB,EAAA,iBAAiB,CACK,EAAM,CAAK,EAE1C,GAAS,EAAU,KAAK,CAAO,CACrC,CAOF,GAJI,EAAO,OACT,EAAU,KAAK,EAAc,EAAI,EAAO,KAAK,CAAC,EAG5C,EAAO,MACT,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,EAAO,KAAK,EAAG,CACvD,IAAM,EAAU,EAAW,EAAI,EAAK,CAAK,EAErC,GAAS,EAAU,KAAK,CAAO,CACrC,CAGF,GAAI,EAAO,GAAI,CACb,GAAM,CAAE,OAAQ,EAAI,GAAG,GAAoB,GAAW,CAAC,EAEvD,IAAK,IAAM,KAAS,OAAO,KAAK,EAAO,EAAE,EAAoC,CAC3E,IAAM,EAAW,EAAO,GAAG,GAEtB,GAEL,EAAU,KAAK,EAAA,OAAO,EAAI,EAAiB,EAA2B,CAAe,CAAC,CACxF,CACF,CAEA,IAAM,MAAsB,CAC1B,IAAK,IAAM,KAAW,EAAW,EAAQ,CAC3C,EAIA,OAFA,EAAA,mBAAmB,CAAO,EAEnB,CACT,EAEM,EAAa,EAAA,qBAEb,GACJ,EACA,IAC6B,CAC7B,GAAI,OAAO,GAAU,WACnB,OAAO,EAAA,gBAAkB,CACvB,EAAQ,EAAM,CAAC,CAEjB,CAAC,EAGH,IAAA,EAAI,EAAA,WAAA,CAAW,CAAK,EAClB,OAAO,EAAA,gBAAkB,CACvB,EAAQ,EAAM,KAAK,CAErB,CAAC,EAGH,EAAQ,CAAK,CACf,EAEA,SAAS,EAAe,EAAmB,EAAc,EAAmD,CAC1G,OAAO,EAAqB,EAAQ,GAAS,EAAA,QAAQ,EAAM,EAAM,CAAI,CAAC,CACxE,CAEA,SAAS,EAAW,EAAmB,EAAc,EAAmD,CACtG,IAAM,EAAU,EAAA,iBAAiB,EAAK,WAAW,IAAI,EAAI,EAAO,EAAA,QAAQ,CAAI,CAAC,EAE7E,GAAI,CAAC,EAAS,OAEd,IAAI,EAAQ,GAQZ,OAAO,EAAqB,EAPV,GAA0D,CACtE,GAAK,MAAQ,IAAM,IACrB,EAAQ,GACR,EAAK,MAAM,YAAY,EAAS,EAAA,iBAAiB,OAAO,CAAC,CAAC,CAAC,GAClD,GAAO,EAAK,MAAM,eAAe,CAAO,CACrD,CAE2C,CAC7C,CAEA,SAAS,EACP,EACA,EACY,CACZ,IAAM,EACJ,OAAO,GAAU,WACb,MAC+B,CAC7B,IAAM,EAAkC,CAAC,EAEzC,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,CAAK,EAC7C,EAAO,GAAO,EAAA,qBAAqB,CAAK,EAG1C,OAAO,CACT,EAEF,EAAO,IAAI,IAkBf,OAhBY,EAAA,gBAAkB,CAC5B,IAAM,EAAO,IAAI,IAEjB,IAAK,GAAM,CAAC,EAAK,KAAW,OAAO,QAAQ,EAAO,CAAC,EAC5C,IAEL,EAAK,IAAI,CAAG,EAEP,EAAK,IAAI,CAAG,GAAG,EAAK,UAAU,IAAI,CAAG,GAE5C,IAAK,IAAM,KAAO,EACX,EAAK,IAAI,CAAG,GAAG,EAAK,UAAU,OAAO,CAAG,EAE/C,EAAO,CACT,CAEO,CACT"}