{"version":3,"file":"manager.utils.cjs","names":[],"sources":["../src/manager.utils.ts"],"sourcesContent":["import type { ParsedHotkey } from './hotkey'\n\n/**\n * Behavior when registering a hotkey/sequence that conflicts with an existing registration.\n *\n * - `'warn'` - Log a warning to the console but allow both registrations (default)\n * - `'error'` - Throw an error and prevent the new registration\n * - `'replace'` - Unregister the existing registration and register the new one\n * - `'allow'` - Allow multiple registrations without warning\n */\nexport type ConflictBehavior = 'warn' | 'error' | 'replace' | 'allow'\n\n/**\n * Default options for hotkey/sequence registration.\n * Omitted: platform, target (resolved at registration), requireReset (HotkeyManager only).\n */\nexport const defaultHotkeyOptions = {\n  preventDefault: true,\n  stopPropagation: true,\n  eventType: 'keydown' as const,\n  enabled: true,\n  ignoreInputs: true,\n  conflictBehavior: 'warn' as ConflictBehavior,\n}\n\n/**\n * Computes the default ignoreInputs value based on the hotkey.\n * Ctrl/Meta shortcuts and Escape fire in inputs; single keys and Shift/Alt combos are ignored.\n */\nexport function getDefaultIgnoreInputs(parsedHotkey: ParsedHotkey): boolean {\n  if (parsedHotkey.ctrl || parsedHotkey.meta) return false // Mod+S, Ctrl+C, etc.\n  if (parsedHotkey.key === 'Escape') return false // Close modal, etc.\n  return true // Single keys, Shift+key, Alt+key\n}\n\n/**\n * Checks if an element is an input-like element that should be ignored for hotkeys.\n *\n * This includes:\n * - HTMLInputElement (all input types except button, submit, reset)\n * - HTMLTextAreaElement\n * - HTMLSelectElement\n * - Elements with contentEditable enabled\n *\n * Button-type inputs (button, submit, reset) are excluded so hotkeys like\n * Mod+S and Escape fire when the user has tabbed to a form button.\n */\nexport function isInputElement(element: EventTarget | null): boolean {\n  if (!element) {\n    return false\n  }\n\n  if (element instanceof HTMLInputElement) {\n    const type = element.type.toLowerCase()\n    if (type === 'button' || type === 'submit' || type === 'reset') {\n      return false\n    }\n    return true\n  }\n\n  if (\n    element instanceof HTMLTextAreaElement ||\n    element instanceof HTMLSelectElement\n  ) {\n    return true\n  }\n\n  // Check for contenteditable elements (includes \"true\", \"\", \"plaintext-only\",\n  // and inherited contenteditable from ancestor elements)\n  if (element instanceof HTMLElement && element.isContentEditable) {\n    return true\n  }\n\n  return false\n}\n\n/**\n * Returns the focused element for the document associated with where hotkey\n * listeners are attached (listener root). Use this instead of the global\n * `document.activeElement` so registrations scoped to an iframe (or another\n * document) read focus from the correct tree.\n */\nexport function getActiveElementForListenerTarget(\n  target: HTMLElement | Document | Window,\n): Element | null {\n  if (typeof document === 'undefined') {\n    return null\n  }\n\n  // Document first (nodeType covers environments where `document instanceof Document` is false)\n  if (\n    (typeof Document !== 'undefined' && target instanceof Document) ||\n    (typeof Node !== 'undefined' &&\n      (target as Node).nodeType === Node.DOCUMENT_NODE)\n  ) {\n    return (target as Document).activeElement\n  }\n\n  if (typeof HTMLElement !== 'undefined' && target instanceof HTMLElement) {\n    return target.ownerDocument.activeElement ?? null\n  }\n\n  // Window (global or iframe): avoid relying on `instanceof Window` alone (test DOM quirks)\n  return (target as Window).document.activeElement ?? null\n}\n\n/**\n * Returns whether an event should be ignored because it originated from an\n * input-like element other than the registration target.\n *\n * This checks:\n * - the currently focused element for the listener target\n * - the event's composed path (for shadow DOM)\n * - the event target as a final fallback\n */\nexport function shouldIgnoreInputEvent(\n  event: KeyboardEvent,\n  listenerTarget: HTMLElement | Document | Window,\n  registrationTarget: HTMLElement | Document | Window,\n): boolean {\n  const focused = getActiveElementForListenerTarget(listenerTarget)\n  if (focused && isInputElement(focused) && focused !== registrationTarget) {\n    return true\n  }\n\n  if (\n    event\n      .composedPath()\n      .some(\n        (element) => isInputElement(element) && element !== registrationTarget,\n      )\n  ) {\n    return true\n  }\n\n  return isInputElement(event.target) && event.target !== registrationTarget\n}\n\n/**\n * Checks if an event is for the given target (originated from or bubbled to it).\n *\n * For document/window targets, also accepts document.documentElement as currentTarget\n * to handle Brave and other browsers where currentTarget may be documentElement\n * instead of document when listeners are attached to document.\n */\nexport function isEventForTarget(\n  event: KeyboardEvent,\n  target: HTMLElement | Document | Window,\n): boolean {\n  // For Document and Window, verify that our handler was indeed called for this target.\n  //\n  // Browser compatibility note:\n  // Per the DOM spec, event.currentTarget should equal the element the listener was\n  // attached to. However, some Chromium-based browsers (notably Brave) exhibit\n  // non-standard behavior where event.currentTarget is set to document.documentElement\n  // (<html>) instead of document when a listener is attached to document.\n  // This may be related to privacy/fingerprinting protections.\n  //\n  // To ensure cross-browser compatibility, we accept both the expected target\n  // and document.documentElement as valid currentTarget values.\n  // See: https://dom.spec.whatwg.org/#dom-event-currenttarget\n  if (target === document || target === window) {\n    return (\n      event.currentTarget === target ||\n      event.currentTarget === document.documentElement\n    )\n  }\n\n  // For Window, accept window, document, or document.documentElement (browser quirks)\n  if (target === window) {\n    return (\n      event.currentTarget === window ||\n      event.currentTarget === document ||\n      event.currentTarget === document.documentElement\n    )\n  }\n\n  // For HTMLElement, check if event originated from or bubbled to the element\n  if (target instanceof HTMLElement) {\n    // Check if the event's currentTarget is the target (capturing/bubbling)\n    if (event.currentTarget === target) {\n      return true\n    }\n\n    // Check if the event's target is a descendant of our target\n    if (event.target instanceof Node && target.contains(event.target)) {\n      return true\n    }\n  }\n\n  return false\n}\n\n/**\n * Handles conflicts between registrations based on conflict behavior.\n *\n * @param conflictingId - The ID of the conflicting registration\n * @param keyDisplay - Display string for the conflicting key/sequence (for error messages)\n * @param conflictBehavior - How to handle the conflict\n * @param unregister - Function to unregister by ID\n */\nexport function handleConflict(\n  conflictingId: string,\n  keyDisplay: string,\n  conflictBehavior: ConflictBehavior,\n  unregister: (id: string) => void,\n): void {\n  if (conflictBehavior === 'allow') {\n    return\n  }\n\n  if (conflictBehavior === 'warn') {\n    console.warn(\n      `'${keyDisplay}' is already registered. Multiple handlers will be triggered. ` +\n        `Use conflictBehavior: 'replace' to replace the existing handler, ` +\n        `or conflictBehavior: 'allow' to suppress this warning.`,\n    )\n    return\n  }\n\n  if (conflictBehavior === 'error') {\n    throw new Error(\n      `'${keyDisplay}' is already registered. ` +\n        `Use conflictBehavior: 'replace' to replace the existing handler, ` +\n        `or conflictBehavior: 'allow' to allow multiple registrations.`,\n    )\n  }\n\n  // At this point, conflictBehavior must be 'replace'\n  unregister(conflictingId)\n}\n"],"mappings":";;;;;;AAgBA,MAAa,uBAAuB;CAClC,gBAAgB;CAChB,iBAAiB;CACjB,WAAW;CACX,SAAS;CACT,cAAc;CACd,kBAAkB;CACnB;;;;;AAMD,SAAgB,uBAAuB,cAAqC;AAC1E,KAAI,aAAa,QAAQ,aAAa,KAAM,QAAO;AACnD,KAAI,aAAa,QAAQ,SAAU,QAAO;AAC1C,QAAO;;;;;;;;;;;;;;AAeT,SAAgB,eAAe,SAAsC;AACnE,KAAI,CAAC,QACH,QAAO;AAGT,KAAI,mBAAmB,kBAAkB;EACvC,MAAM,OAAO,QAAQ,KAAK,aAAa;AACvC,MAAI,SAAS,YAAY,SAAS,YAAY,SAAS,QACrD,QAAO;AAET,SAAO;;AAGT,KACE,mBAAmB,uBACnB,mBAAmB,kBAEnB,QAAO;AAKT,KAAI,mBAAmB,eAAe,QAAQ,kBAC5C,QAAO;AAGT,QAAO;;;;;;;;AAST,SAAgB,kCACd,QACgB;AAChB,KAAI,OAAO,aAAa,YACtB,QAAO;AAIT,KACG,OAAO,aAAa,eAAe,kBAAkB,YACrD,OAAO,SAAS,eACd,OAAgB,aAAa,KAAK,cAErC,QAAQ,OAAoB;AAG9B,KAAI,OAAO,gBAAgB,eAAe,kBAAkB,YAC1D,QAAO,OAAO,cAAc,iBAAiB;AAI/C,QAAQ,OAAkB,SAAS,iBAAiB;;;;;;;;;;;AAYtD,SAAgB,uBACd,OACA,gBACA,oBACS;CACT,MAAM,UAAU,kCAAkC,eAAe;AACjE,KAAI,WAAW,eAAe,QAAQ,IAAI,YAAY,mBACpD,QAAO;AAGT,KACE,MACG,cAAc,CACd,MACE,YAAY,eAAe,QAAQ,IAAI,YAAY,mBACrD,CAEH,QAAO;AAGT,QAAO,eAAe,MAAM,OAAO,IAAI,MAAM,WAAW;;;;;;;;;AAU1D,SAAgB,iBACd,OACA,QACS;AAaT,KAAI,WAAW,YAAY,WAAW,OACpC,QACE,MAAM,kBAAkB,UACxB,MAAM,kBAAkB,SAAS;AAKrC,KAAI,WAAW,OACb,QACE,MAAM,kBAAkB,UACxB,MAAM,kBAAkB,YACxB,MAAM,kBAAkB,SAAS;AAKrC,KAAI,kBAAkB,aAAa;AAEjC,MAAI,MAAM,kBAAkB,OAC1B,QAAO;AAIT,MAAI,MAAM,kBAAkB,QAAQ,OAAO,SAAS,MAAM,OAAO,CAC/D,QAAO;;AAIX,QAAO;;;;;;;;;;AAWT,SAAgB,eACd,eACA,YACA,kBACA,YACM;AACN,KAAI,qBAAqB,QACvB;AAGF,KAAI,qBAAqB,QAAQ;AAC/B,UAAQ,KACN,IAAI,WAAW,uLAGhB;AACD;;AAGF,KAAI,qBAAqB,QACvB,OAAM,IAAI,MACR,IAAI,WAAW,yJAGhB;AAIH,YAAW,cAAc"}