{"version":3,"file":"OverlayController.mjs","names":[],"sources":["../../../../../../../../src/lib/controller/overlay/OverlayController.ts"],"sourcesContent":["import {\n  action,\n  computed,\n  makeObservable,\n  observable,\n  runInAction,\n} from \"mobx\";\nimport useSelector from \"@/lib/mobx/useSelector\";\nimport { useStatic } from \"@/lib/hooks/useStatic\";\nimport { useEffect, useRef, type DependencyList } from \"react\";\nimport { useCloseOverlayConfirmationController } from \"@/lib/controller/overlay/useCloseOverlayConfirmationController\";\nimport type { FlowComponentName } from \"@/components/propTypes\";\n\nexport type OverlayOpenHandler = () => unknown;\nexport type OverlayCloseHandler = () => unknown;\nexport type OverlayOpenStateHandler = (isOpen: boolean) => unknown;\ntype AnyOverlayOpenStateHandler =\n  OverlayOpenHandler | OverlayCloseHandler | OverlayOpenStateHandler;\n\ntype DisposerFn = () => void;\n\nexport interface CloseOverlayOptions {\n  /**\n   * The overlay to close. Omitted, the nearest enclosing overlay is closed –\n   * the same thing `closeOverlay={true}` means.\n   */\n  overlay?: FlowComponentName | OverlayController;\n  bypassConfirmation?: boolean;\n}\n\nexport interface OnOverlayClosedOptions {\n  dependencies?: DependencyList;\n}\n\nexport type CloseModalOptions = Omit<CloseOverlayOptions, \"overlay\">;\n\n/**\n * Every way an action can point at an overlay: by the Flow component name it\n * registers under, by controller, by options, or with `true` – the nearest\n * enclosing overlay, whatever its name.\n */\nexport type OverlayReference =\n  FlowComponentName | OverlayController | CloseOverlayOptions | true;\n\ntype CloseOptions = CloseOverlayOptions | CloseModalOptions;\n\nexport interface OverlayControllerOptions {\n  isDefaultOpen?: boolean;\n  onOpen?: OverlayOpenHandler;\n  onClose?: OverlayCloseHandler;\n  onOpenChange?: OverlayOpenStateHandler;\n  /**\n   * Whether closing the overlay must be confirmed. `undefined` means \"no\n   * opinion\": the value is not contributed at all, so other sources (e.g. a\n   * `Modal`'s `confirmOnClose` prop or a dirty `Form`) still decide.\n   */\n  confirmOnClose?: boolean;\n}\n\ntype ConstructorOptions = Pick<\n  OverlayControllerOptions,\n  \"isDefaultOpen\" | \"confirmOnClose\"\n>;\n\nexport class OverlayController {\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: ConstructorOptions = {}) {\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  public useUpdateOptions(options: OverlayControllerOptions = {}): void {\n    const { onOpen, onClose, onOpenChange, confirmOnClose } = options;\n\n    this.useOnHandler(onOpen, (h) =>\n      this.addOpenStateHandler(h, this.onOpenHandlers),\n    );\n    this.useOnHandler(onClose, (h) =>\n      this.addOpenStateHandler(h, this.onCloseHandlers),\n    );\n    this.useOnHandler(onOpenChange, (h) =>\n      this.addOpenStateHandler(h, this.onOpenChangeHandlers),\n    );\n\n    this.useConfirmOnCloseSource(confirmOnClose);\n  }\n\n  /**\n   * Registers the calling component as a close confirmation source for as long\n   * as it is mounted. `undefined` contributes nothing at all.\n   */\n  private useConfirmOnCloseSource(confirmOnClose: boolean | undefined): void {\n    const source = useStatic((): object => ({}));\n\n    useEffect(() => {\n      if (confirmOnClose === undefined) {\n        this.removeConfirmOnCloseSource(source);\n        return;\n      }\n      this.setConfirmOnCloseSource(source, confirmOnClose);\n      return () => this.removeConfirmOnCloseSource(source);\n    }, [this, source, confirmOnClose]);\n  }\n\n  private setConfirmOnCloseSource(source: object, confirmOnClose: boolean) {\n    runInAction(() => this.confirmOnCloseSources.set(source, confirmOnClose));\n  }\n\n  private removeConfirmOnCloseSource(source: object) {\n    runInAction(() => this.confirmOnCloseSources.delete(source));\n  }\n\n  /**\n   * Can be used to execute a callback when the Overlay has unmounted – means\n   * after any closing animation.\n   *\n   * NOTICE: This hook only works inside the corresponding Overlay!\n   */\n  public useOnClosed(\n    callback: () => unknown,\n    options: OnOverlayClosedOptions = {},\n  ) {\n    const { dependencies = [] } = options;\n    const isOpen = this.useIsOpen();\n    const wasOpen = useRef(this.isOpen);\n\n    this.useUpdateOptions({\n      onOpen: () => {\n        wasOpen.current = true;\n      },\n    });\n\n    useEffect(() => {\n      return () => {\n        if (!isOpen && wasOpen.current) {\n          wasOpen.current = false;\n          callback();\n        }\n      };\n    }, [isOpen, ...dependencies]);\n  }\n\n  public static useNew(\n    options: OverlayControllerOptions = {},\n  ): OverlayController {\n    const controller = useStatic(() => new OverlayController(options));\n    controller.useUpdateOptions(options);\n    return controller;\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 useOnHandler<T extends AnyOverlayOpenStateHandler>(\n    handler: T | undefined,\n    addHandlerFn: (handler: T) => DisposerFn,\n  ) {\n    useEffect(\n      () => (handler ? addHandlerFn(handler) : undefined),\n      [handler, this],\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?: CloseOptions): 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: CloseOptions = {}): 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.\n   */\n  public syncOpen(toOpen: boolean): void {\n    this.isOpen = toOpen;\n  }\n\n  private applyOpenState(toOpen: boolean, options: CloseOptions): 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.isOpen = toOpen;\n    }\n  }\n\n  public setIsContentSuspended(to: boolean): void {\n    this.isContentSuspended = to;\n  }\n\n  public useIsOpen() {\n    return useSelector(() => this.isOpen);\n  }\n\n  public useIsContentSuspended() {\n    return useSelector(() => this.isContentSuspended);\n  }\n\n  public useShowConfirmationModal() {\n    return useSelector(() => this.showConfirmationModal);\n  }\n\n  public useConfirmOnCloseEnabled() {\n    return useSelector(() => this.confirmOnCloseEnabled);\n  }\n\n  public useConfirmationController() {\n    return useCloseOverlayConfirmationController(this);\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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"}