{"version":3,"file":"OverlayState.mjs","names":[],"sources":["../../../../../../../components-base/src/overlay/OverlayState.ts"],"sourcesContent":["import {\n  action,\n  computed,\n  makeObservable,\n  observable,\n  runInAction,\n} from \"mobx\";\n\nexport type OverlayOpenHandler = () => unknown;\nexport type OverlayCloseHandler = () => unknown;\nexport type OverlayOpenStateHandler = (isOpen: boolean) => unknown;\nexport type AnyOverlayOpenStateHandler =\n  OverlayOpenHandler | OverlayCloseHandler | OverlayOpenStateHandler;\n\nexport type DisposerFn = () => void;\n\nexport interface OverlayCloseOptions {\n  /** Close without asking, even when the overlay requires confirmation. */\n  bypassConfirmation?: boolean;\n}\n\nexport interface OverlayStateOptions {\n  isDefaultOpen?: boolean;\n  /**\n   * Whether closing the overlay must be confirmed while no source has an\n   * opinion (`setConfirmOnCloseSource`).\n   */\n  confirmOnClose?: boolean;\n}\n\n/**\n * Whether an overlay is open, and whether closing it has to be confirmed first.\n * Each binding adds its subscription and its lifecycle: React's\n * `OverlayController` subclasses it, Vue's adapter wraps it in refs.\n *\n * `TCloseOptions` lets a binding pass more on a close than the state reads —\n * React's `overlay`, resolved by `Action` before the close arrives here.\n */\nexport class OverlayState<\n  TCloseOptions extends OverlayCloseOptions = OverlayCloseOptions,\n> {\n  public isOpen = false;\n  public isContentSuspended = false;\n  private onOpenHandlers = new Set<OverlayOpenHandler>();\n  private onCloseHandlers = new Set<OverlayCloseHandler>();\n  private onOpenChangeHandlers = new Set<OverlayOpenStateHandler>();\n\n  public showConfirmationModal = false;\n  public closeIsConfirmed = false;\n  private readonly confirmOnCloseFromOptions: boolean;\n  /**\n   * Every mounted component that has an opinion on close confirmation\n   * contributes one entry here – e.g. a `Modal` with a `confirmOnClose` prop\n   * and a `Form` tracking its dirty state. Combining them instead of assigning\n   * a single flag keeps them from overwriting each other.\n   */\n  private confirmOnCloseSources = observable.map<object, boolean>();\n  /**\n   * Active grants that allow closing without a confirmation. Each entry is\n   * scoped to an ongoing operation that legitimately closes the overlay – e.g.\n   * a `Form` submit – and is dropped again when that operation has finished, so\n   * an operation that does _not_ close the overlay leaves the confirmation\n   * armed.\n   */\n  private closeWithoutConfirmationGrants = new Set<object>();\n  /**\n   * The open state a `setOpen` call is currently applying. Handlers run before\n   * `isOpen` is assigned, so a handler that calls `setOpen` again would not be\n   * caught by the equality check at the top and would recurse.\n   */\n  private openStateInFlight: boolean | undefined;\n\n  public constructor(options: OverlayStateOptions = {}) {\n    makeObservable(this, {\n      isOpen: observable,\n      isContentSuspended: observable,\n      showConfirmationModal: observable,\n      confirmOnCloseEnabled: computed,\n      open: action.bound,\n      close: action.bound,\n      toggle: action.bound,\n      setOpen: action.bound,\n      syncOpen: action.bound,\n      setIsContentSuspended: action.bound,\n      confirmClose: action.bound,\n      cancelConfirmation: action.bound,\n    });\n    const { isDefaultOpen = false, confirmOnClose = false } = options;\n    this.isOpen = isDefaultOpen;\n    this.confirmOnCloseFromOptions = confirmOnClose;\n  }\n\n  /** Whether closing this overlay currently requires a confirmation. */\n  public get confirmOnCloseEnabled(): boolean {\n    if (this.confirmOnCloseSources.size === 0) {\n      return this.confirmOnCloseFromOptions;\n    }\n    return Array.from(this.confirmOnCloseSources.values()).some(Boolean);\n  }\n\n  /**\n   * Sets the opinion of one source – any object the caller keeps – on close\n   * confirmation. Remove the source again when its owner goes away.\n   *\n   * @internal Bindings call it from their lifecycle.\n   */\n  public setConfirmOnCloseSource(source: object, confirmOnClose: boolean) {\n    runInAction(() => {\n      this.confirmOnCloseSources.set(source, confirmOnClose);\n      this.withdrawUnneededConfirmation();\n    });\n  }\n\n  /** @internal Bindings call it from their lifecycle. */\n  public removeConfirmOnCloseSource(source: object) {\n    runInAction(() => {\n      this.confirmOnCloseSources.delete(source);\n      this.withdrawUnneededConfirmation();\n    });\n  }\n\n  private withdrawUnneededConfirmation() {\n    if (!this.confirmOnCloseEnabled) {\n      this.showConfirmationModal = false;\n    }\n  }\n\n  private addOpenStateHandler<T extends AnyOverlayOpenStateHandler>(\n    handler: T,\n    handlersSet: Set<T>,\n  ): DisposerFn {\n    handlersSet.add(handler);\n    return () => {\n      handlersSet.delete(handler);\n    };\n  }\n\n  private executeHandlers(\n    isOpen: boolean,\n    handlers: Set<\n      OverlayOpenHandler | OverlayCloseHandler | OverlayOpenStateHandler\n    >,\n  ): boolean {\n    const handlerResult = Array.from(handlers).map((handler) =>\n      handler(isOpen),\n    );\n    return handlerResult.some((result) => result === false);\n  }\n\n  private executeOnClose(): boolean {\n    return this.executeHandlers(false, this.onCloseHandlers);\n  }\n\n  private executeOnOpen(): boolean {\n    return this.executeHandlers(true, this.onOpenHandlers);\n  }\n\n  private executeOnOpenChange(isOpen: boolean): boolean {\n    return this.executeHandlers(isOpen, this.onOpenChangeHandlers);\n  }\n\n  public addOnClose(handler: OverlayCloseHandler) {\n    return this.addOpenStateHandler(handler, this.onCloseHandlers);\n  }\n\n  public addOnOpen(handler: OverlayOpenHandler) {\n    return this.addOpenStateHandler(handler, this.onOpenHandlers);\n  }\n\n  public addOnOpenChange(handler: OverlayOpenStateHandler) {\n    return this.addOpenStateHandler(handler, this.onOpenChangeHandlers);\n  }\n\n  public open(): void {\n    this.setOpen(true);\n  }\n\n  public close(options?: TCloseOptions): void {\n    this.setOpen(false, options);\n  }\n\n  public toggle(): void {\n    this.setOpen(!this.isOpen);\n  }\n\n  public setOpen(toOpen: boolean, options?: TCloseOptions): void {\n    if (this.isOpen === toOpen || this.openStateInFlight === toOpen) {\n      return;\n    }\n\n    this.openStateInFlight = toOpen;\n    try {\n      this.applyOpenState(toOpen, options);\n    } finally {\n      this.openStateInFlight = undefined;\n    }\n  }\n\n  /**\n   * Sets the open state without running any handler. Use it to mirror a\n   * controlled `isOpen` prop, whose change comes from the consumer – notifying\n   * them of their own state would echo it back. Closing this way drops a\n   * pending close confirmation as well.\n   */\n  public syncOpen(toOpen: boolean): void {\n    this.assignOpenState(toOpen);\n  }\n\n  /** A closed overlay has no close left to confirm. */\n  private assignOpenState(toOpen: boolean): void {\n    this.isOpen = toOpen;\n    if (!toOpen) {\n      this.showConfirmationModal = false;\n      this.closeIsConfirmed = false;\n    }\n  }\n\n  private applyOpenState(toOpen: boolean, options?: TCloseOptions): void {\n    const { bypassConfirmation = false } = options ?? {};\n\n    if (toOpen === false) {\n      // The confirmation is a one-shot permission for exactly this close\n      // attempt – consume it here instead of only when a close succeeds, so it\n      // cannot survive an aborted close and disarm later attempts.\n      const closeIsConfirmed = this.closeIsConfirmed;\n      this.closeIsConfirmed = false;\n\n      if (\n        this.confirmOnCloseEnabled &&\n        !closeIsConfirmed &&\n        this.closeWithoutConfirmationGrants.size === 0 &&\n        !bypassConfirmation\n      ) {\n        this.showConfirmationModal = true;\n        return;\n      }\n    }\n\n    let aborted: boolean;\n    if (toOpen) {\n      aborted = this.executeOnOpen();\n    } else {\n      aborted = this.executeOnClose();\n    }\n    if (!aborted) {\n      aborted = this.executeOnOpenChange(toOpen);\n    }\n\n    if (!aborted) {\n      this.assignOpenState(toOpen);\n    }\n  }\n\n  public setIsContentSuspended(to: boolean): void {\n    this.isContentSuspended = to;\n  }\n\n  /**\n   * Allows closing this overlay without a confirmation until the returned\n   * disposer is called. Use it around an operation that may legitimately close\n   * the overlay – e.g. a `Form` submit, where a \"discard unsaved changes?\"\n   * prompt would be nonsense – and dispose it as soon as that operation has\n   * finished. An operation that ends without closing the overlay therefore\n   * leaves the confirmation armed (#2775).\n   */\n  public grantCloseWithoutConfirmation(): DisposerFn {\n    const grant = {};\n    this.closeWithoutConfirmationGrants.add(grant);\n    return () => {\n      this.closeWithoutConfirmationGrants.delete(grant);\n    };\n  }\n\n  public confirmClose(): void {\n    this.closeIsConfirmed = true;\n    this.showConfirmationModal = false;\n  }\n\n  public cancelConfirmation(): void {\n    this.showConfirmationModal = false;\n  }\n}\n"],"mappings":"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"}