@okyrychenko-dev/react-action-guard-router 0.2.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +228 -0
- package/README.md +187 -110
- package/dist/index.cjs +30 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -101
- package/dist/index.d.ts +9 -101
- package/dist/index.js +28 -17
- package/dist/index.js.map +1 -1
- package/dist/nextjs/index.cjs +89 -53
- package/dist/nextjs/index.cjs.map +1 -1
- package/dist/nextjs/index.d.cts +1 -100
- package/dist/nextjs/index.d.ts +1 -100
- package/dist/nextjs/index.js +83 -47
- package/dist/nextjs/index.js.map +1 -1
- package/dist/react-router/index.cjs +69 -46
- package/dist/react-router/index.cjs.map +1 -1
- package/dist/react-router/index.d.cts +1 -87
- package/dist/react-router/index.d.ts +1 -87
- package/dist/react-router/index.js +69 -46
- package/dist/react-router/index.js.map +1 -1
- package/dist/tanstack-router/index.cjs +57 -105
- package/dist/tanstack-router/index.cjs.map +1 -1
- package/dist/tanstack-router/index.d.cts +2 -63
- package/dist/tanstack-router/index.d.ts +2 -63
- package/dist/tanstack-router/index.js +58 -106
- package/dist/tanstack-router/index.js.map +1 -1
- package/dist/{types-C3CPuZip.d.cts → types-xxRwsJ1N.d.cts} +0 -35
- package/dist/{types-C3CPuZip.d.ts → types-xxRwsJ1N.d.ts} +0 -35
- package/package.json +80 -66
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
var reactRouter = require('@tanstack/react-router');
|
|
4
4
|
var react = require('react');
|
|
5
|
+
var typeUtils = require('@okyrychenko-dev/type-utils');
|
|
5
6
|
var reactActionGuard = require('@okyrychenko-dev/react-action-guard');
|
|
6
7
|
|
|
7
8
|
// src/tanstack-router/useNavigationBlocker.ts
|
|
8
|
-
|
|
9
|
-
// src/core/utils.ts
|
|
10
9
|
function resolveCondition(condition) {
|
|
11
|
-
return
|
|
10
|
+
return typeUtils.isFunction(condition) ? condition() : condition;
|
|
12
11
|
}
|
|
13
|
-
var hasThen = (value) =>
|
|
12
|
+
var hasThen = (value) => typeUtils.isObject(value) && "then" in value;
|
|
14
13
|
function isThenable(value) {
|
|
15
14
|
if (!hasThen(value)) {
|
|
16
15
|
return false;
|
|
17
16
|
}
|
|
18
|
-
return
|
|
17
|
+
return typeUtils.isFunction(value.then);
|
|
19
18
|
}
|
|
20
19
|
function resolveConfirmResult(message, onConfirm, fallbackConfirm) {
|
|
21
20
|
if (!onConfirm) {
|
|
@@ -27,144 +26,97 @@ function resolveConfirmResult(message, onConfirm, fallbackConfirm) {
|
|
|
27
26
|
}
|
|
28
27
|
return { kind: "sync", confirmed: result };
|
|
29
28
|
}
|
|
30
|
-
function isDefined(value) {
|
|
31
|
-
return value !== void 0 && value !== null;
|
|
32
|
-
}
|
|
33
29
|
function normalizeScope(scope) {
|
|
34
30
|
if (!scope) {
|
|
35
31
|
return [];
|
|
36
32
|
}
|
|
37
|
-
return
|
|
33
|
+
return typeUtils.isArray(scope) ? scope : [scope];
|
|
38
34
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
var DEFAULT_UNLOAD_MESSAGE = "Changes you made may not be saved.";
|
|
42
|
-
function useBeforeUnload(when, message = DEFAULT_UNLOAD_MESSAGE) {
|
|
43
|
-
react.useEffect(() => {
|
|
44
|
-
const shouldBlock = resolveCondition(when);
|
|
45
|
-
if (!shouldBlock) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
const handleBeforeUnload = (event) => {
|
|
49
|
-
event.preventDefault();
|
|
50
|
-
if ("returnValue" in event) {
|
|
51
|
-
event.returnValue = message;
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
55
|
-
return () => {
|
|
56
|
-
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
57
|
-
};
|
|
58
|
-
}, [when, message]);
|
|
35
|
+
function createScopeKey(scope) {
|
|
36
|
+
return JSON.stringify([...new Set(normalizeScope(scope))].sort());
|
|
59
37
|
}
|
|
60
38
|
function useShouldBlock(when, scope) {
|
|
61
39
|
const scopes = react.useMemo(() => normalizeScope(scope), [scope]);
|
|
62
40
|
const isScopeBlocked = reactActionGuard.useIsBlocked(scopes.length === 1 ? scopes[0] : scopes);
|
|
63
41
|
const warnedMissingConditionRef = react.useRef(false);
|
|
64
|
-
const warnedFunctionRef = react.useRef(false);
|
|
65
42
|
react.useEffect(() => {
|
|
66
43
|
if (process.env.NODE_ENV !== "development") {
|
|
67
44
|
return;
|
|
68
45
|
}
|
|
69
|
-
if (!warnedMissingConditionRef.current && !isDefined(when) && !scope) {
|
|
46
|
+
if (!warnedMissingConditionRef.current && !typeUtils.isDefined(when) && !scope) {
|
|
70
47
|
warnedMissingConditionRef.current = true;
|
|
71
48
|
console.warn(
|
|
72
49
|
'[react-action-guard-router] useShouldBlock: Neither "when" nor "scope" provided. This hook will always return false. Please provide at least one condition.'
|
|
73
50
|
);
|
|
74
51
|
}
|
|
75
|
-
if (!warnedFunctionRef.current && typeof when === "function") {
|
|
76
|
-
warnedFunctionRef.current = true;
|
|
77
|
-
console.warn(
|
|
78
|
-
'[react-action-guard-router] useShouldBlock: "when" is a function. Make sure to wrap it with useCallback to avoid unnecessary re-computations.'
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
52
|
}, [when, scope]);
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
if (isDefined(when)) {
|
|
85
|
-
blockCondition = resolveCondition(when);
|
|
86
|
-
}
|
|
87
|
-
if (scope && isScopeBlocked) {
|
|
88
|
-
blockCondition = true;
|
|
89
|
-
}
|
|
90
|
-
return blockCondition;
|
|
91
|
-
}, [when, scope, isScopeBlocked]);
|
|
92
|
-
return shouldBlock;
|
|
53
|
+
const blockCondition = typeUtils.isDefined(when) && resolveCondition(when);
|
|
54
|
+
return blockCondition || Boolean(scope && isScopeBlocked);
|
|
93
55
|
}
|
|
94
56
|
|
|
95
|
-
// src/
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (!("history" in value)) {
|
|
101
|
-
return false;
|
|
57
|
+
// src/core/confirmationOwner.utils.ts
|
|
58
|
+
function createConfirmationOwner() {
|
|
59
|
+
let currentAttempt = 0;
|
|
60
|
+
function invalidate() {
|
|
61
|
+
currentAttempt += 1;
|
|
102
62
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
};
|
|
112
|
-
var hasRetry = (value) => typeof value.retry === "function";
|
|
113
|
-
var attemptRetry = (update) => {
|
|
114
|
-
if (hasRetry(update)) {
|
|
115
|
-
try {
|
|
116
|
-
update.retry();
|
|
117
|
-
} catch {
|
|
63
|
+
function begin() {
|
|
64
|
+
const attempt = ++currentAttempt;
|
|
65
|
+
function settle() {
|
|
66
|
+
if (attempt !== currentAttempt) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
invalidate();
|
|
70
|
+
return true;
|
|
118
71
|
}
|
|
72
|
+
return settle;
|
|
119
73
|
}
|
|
120
|
-
};
|
|
74
|
+
return { begin, invalidate };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/tanstack-router/useNavigationBlocker.ts
|
|
121
78
|
function useNavigationBlocker(options) {
|
|
122
79
|
const { when, scope, message, onBlock, onAllow, blockBrowserUnload = true, onConfirm } = options;
|
|
123
|
-
const router = reactRouter.useRouter();
|
|
124
80
|
const shouldBlock = useShouldBlock(when, scope);
|
|
81
|
+
const scopeKey = createScopeKey(scope);
|
|
82
|
+
const [confirmationOwner] = react.useState(createConfirmationOwner);
|
|
83
|
+
const { begin, invalidate } = confirmationOwner;
|
|
125
84
|
react.useEffect(() => {
|
|
126
|
-
|
|
127
|
-
|
|
85
|
+
return invalidate;
|
|
86
|
+
}, [when, scopeKey, shouldBlock, message, onConfirm, invalidate]);
|
|
87
|
+
const shouldBlockFn = react.useCallback(async () => {
|
|
88
|
+
const settle = begin();
|
|
89
|
+
onBlock?.();
|
|
90
|
+
if (!message) {
|
|
91
|
+
return true;
|
|
128
92
|
}
|
|
129
|
-
|
|
130
|
-
if (!message) {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
onBlock?.();
|
|
93
|
+
try {
|
|
134
94
|
const confirmation = resolveConfirmResult(
|
|
135
95
|
message,
|
|
136
96
|
onConfirm,
|
|
137
97
|
(value) => window.confirm(value)
|
|
138
98
|
);
|
|
99
|
+
let confirmed;
|
|
139
100
|
if (confirmation.kind === "async") {
|
|
140
|
-
confirmation.promise
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
unblock();
|
|
144
|
-
attemptRetry(update);
|
|
145
|
-
}
|
|
146
|
-
}).catch((error) => {
|
|
147
|
-
unblock();
|
|
148
|
-
if (process.env.NODE_ENV !== "production") {
|
|
149
|
-
console.error("[react-action-guard-router] Confirmation error:", error);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
return;
|
|
101
|
+
confirmed = await confirmation.promise;
|
|
102
|
+
} else {
|
|
103
|
+
confirmed = confirmation.confirmed;
|
|
153
104
|
}
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
unblock();
|
|
157
|
-
attemptRetry(update);
|
|
105
|
+
if (!settle() || !confirmed) {
|
|
106
|
+
return true;
|
|
158
107
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
108
|
+
onAllow?.();
|
|
109
|
+
return false;
|
|
110
|
+
} catch {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}, [message, onBlock, onAllow, onConfirm, begin]);
|
|
114
|
+
reactRouter.useBlocker({
|
|
115
|
+
shouldBlockFn,
|
|
116
|
+
enableBeforeUnload: blockBrowserUnload && shouldBlock,
|
|
117
|
+
disabled: !shouldBlock
|
|
118
|
+
});
|
|
119
|
+
return { isBlocking: shouldBlock };
|
|
168
120
|
}
|
|
169
121
|
|
|
170
122
|
exports.useNavigationBlocker = useNavigationBlocker;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/utils.ts","../../src/core/useBeforeUnload.ts","../../src/core/useShouldBlock.ts","../../src/tanstack-router/useNavigationBlocker.ts"],"names":["useEffect","useMemo","useIsBlocked","useRef","useRouter"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Resolves a condition value (boolean or function returning boolean)\n *\n * @param condition - Boolean value or function\n * @returns Resolved boolean value\n */\nexport function resolveCondition(condition: boolean | (() => boolean)): boolean {\n return typeof condition === \"function\" ? condition() : condition;\n}\n\ninterface Thenable {\n then: (...args: Array<unknown>) => unknown;\n}\n\nconst hasThen = (value: unknown): value is Thenable =>\n typeof value === \"object\" && value !== null && \"then\" in value;\n\n/**\n * Checks if a value behaves like a Promise (thenable)\n */\nexport function isThenable<T>(value: unknown): value is PromiseLike<T> {\n if (!hasThen(value)) {\n return false;\n }\n return typeof value.then === \"function\";\n}\n\nexport interface ConfirmResultSync {\n kind: \"sync\";\n confirmed: boolean;\n}\n\nexport interface ConfirmResultAsync {\n kind: \"async\";\n promise: Promise<boolean>;\n}\n\nexport type ConfirmResult = ConfirmResultSync | ConfirmResultAsync;\n\n/**\n * Normalizes custom confirm result into sync or async branch.\n */\nexport function resolveConfirmResult(\n message: string,\n onConfirm: ((message: string) => boolean | PromiseLike<boolean>) | undefined,\n fallbackConfirm: (message: string) => boolean\n): ConfirmResult {\n if (!onConfirm) {\n return { kind: \"sync\", confirmed: fallbackConfirm(message) };\n }\n\n const result = onConfirm(message);\n if (isThenable<boolean>(result)) {\n return { kind: \"async\", promise: Promise.resolve(result) };\n }\n\n return { kind: \"sync\", confirmed: result };\n}\n\n/**\n * Generates a unique blocker ID based on scope and timestamp\n *\n * @param prefix - Prefix for the ID\n * @param scope - Optional scope\n * @returns Unique blocker ID\n */\nexport function createBlockerId(prefix: string, scope?: string | Array<string>): string {\n const scopePart = scope ? `-${Array.isArray(scope) ? scope.join(\"-\") : scope}` : \"\";\n const timestamp = String(Date.now());\n return `${prefix}${scopePart}-${timestamp}`;\n}\n\n/**\n * Checks if a value is defined and not empty\n *\n * @param value - Value to check\n * @returns True if value is defined and not empty\n */\nexport function isDefined<T>(value: T | undefined | null): value is T {\n return value !== undefined && value !== null;\n}\n\n/**\n * Normalizes scope to an array\n *\n * @param scope - String or array of strings\n * @returns Array of scope strings\n */\nexport function normalizeScope(scope: string | Array<string> | undefined): Array<string> {\n if (!scope) {\n return [];\n }\n return Array.isArray(scope) ? scope : [scope];\n}\n","import { useEffect } from \"react\";\nimport { resolveCondition } from \"./utils\";\n\n/**\n * Default message shown when preventing browser unload\n */\nexport const DEFAULT_UNLOAD_MESSAGE = \"Changes you made may not be saved.\";\n\n/**\n * Blocks browser tab close/refresh when condition is met.\n *\n * This hook adds a `beforeunload` event listener to warn users\n * before they close or refresh the browser tab. Modern browsers\n * may ignore custom messages and show their own generic warning.\n *\n * @param when - Boolean or function returning boolean to determine if blocking should be active\n * @param message - Optional custom message (may be ignored by browsers)\n *\n * @example\n * ```tsx\n * function MyForm() {\n * const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);\n *\n * useBeforeUnload(hasUnsavedChanges, 'You have unsaved changes');\n *\n * return <form>...</form>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With function condition\n * function EditPage() {\n * const isFormDirty = useIsBlocked('form');\n *\n * useBeforeUnload(() => isFormDirty);\n *\n * return <div>...</div>;\n * }\n * ```\n */\nexport function useBeforeUnload(\n when: boolean | (() => boolean),\n message = DEFAULT_UNLOAD_MESSAGE\n): void {\n useEffect(() => {\n const shouldBlock = resolveCondition(when);\n\n if (!shouldBlock) {\n return;\n }\n\n const handleBeforeUnload = (event: BeforeUnloadEvent): void => {\n // Prevent default to trigger the browser's confirmation dialog\n event.preventDefault();\n\n // Fallback for legacy browsers/WebViews that still rely on returnValue.\n if (\"returnValue\" in event) {\n // eslint-disable-next-line @typescript-eslint/no-deprecated\n event.returnValue = message;\n }\n };\n\n window.addEventListener(\"beforeunload\", handleBeforeUnload);\n\n return () => {\n window.removeEventListener(\"beforeunload\", handleBeforeUnload);\n };\n }, [when, message]);\n}\n","import { useIsBlocked } from \"@okyrychenko-dev/react-action-guard\";\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { isDefined, normalizeScope, resolveCondition } from \"./utils\";\n\n/**\n * Shared hook to determine if blocking should be active based on\n * condition and/or scope state.\n *\n * This hook uses `useMemo` for optimal performance, ensuring blocking state\n * is computed synchronously without additional re-renders.\n *\n * @param when - Optional condition (boolean or function)\n * @param scope - Optional scope(s) to check\n * @returns Whether blocking should be active\n *\n * @internal\n */\nexport function useShouldBlock(\n when?: boolean | (() => boolean),\n scope?: string | Array<string>\n): boolean {\n // Memoize scope normalization to avoid recalculation on every render\n const scopes = useMemo(() => normalizeScope(scope), [scope]);\n\n // Check if any specified scopes are blocked\n const isScopeBlocked = useIsBlocked(scopes.length === 1 ? scopes[0] : scopes);\n\n // Development-only validation\n const warnedMissingConditionRef = useRef(false);\n const warnedFunctionRef = useRef(false);\n\n useEffect(() => {\n if (process.env.NODE_ENV !== \"development\") {\n return;\n }\n\n if (!warnedMissingConditionRef.current && !isDefined(when) && !scope) {\n warnedMissingConditionRef.current = true;\n console.warn(\n '[react-action-guard-router] useShouldBlock: Neither \"when\" nor \"scope\" provided. ' +\n \"This hook will always return false. Please provide at least one condition.\"\n );\n }\n\n if (!warnedFunctionRef.current && typeof when === \"function\") {\n warnedFunctionRef.current = true;\n console.warn(\n '[react-action-guard-router] useShouldBlock: \"when\" is a function. ' +\n \"Make sure to wrap it with useCallback to avoid unnecessary re-computations.\"\n );\n }\n }, [when, scope]);\n\n // Compute blocking state synchronously using useMemo\n // This avoids the useState + useEffect pattern which causes an extra render\n const shouldBlock = useMemo(() => {\n let blockCondition = false;\n\n // Check manual condition\n if (isDefined(when)) {\n blockCondition = resolveCondition(when);\n }\n\n // Check scope-based blocking (OR logic - block if either condition is true)\n if (scope && isScopeBlocked) {\n blockCondition = true;\n }\n\n return blockCondition;\n }, [when, scope, isScopeBlocked]);\n\n return shouldBlock;\n}\n","import { useRouter } from \"@tanstack/react-router\";\nimport { useEffect } from \"react\";\nimport {\n DEFAULT_UNLOAD_MESSAGE,\n resolveConfirmResult,\n useBeforeUnload,\n useShouldBlock,\n} from \"../core\";\nimport type { NavigationBlockerReturn } from \"../core/types\";\nimport type { SafeTanStackRouter, UseNavigationBlockerOptions } from \"./types\";\n\ninterface RetryableUpdate {\n [key: string]: unknown;\n retry: () => void;\n}\n\nconst hasBlockingHistory = (value: unknown): value is SafeTanStackRouter => {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n if (!(\"history\" in value)) {\n return false;\n }\n const history = value.history;\n if (!history || typeof history !== \"object\") {\n return false;\n }\n if (!(\"block\" in history)) {\n return false;\n }\n return typeof history.block === \"function\";\n};\n\nconst hasRetry = (value: Record<string, unknown>): value is RetryableUpdate =>\n typeof value.retry === \"function\";\n\nconst attemptRetry = (update: Record<string, unknown>): void => {\n if (hasRetry(update)) {\n try {\n update.retry();\n } catch {\n // Swallow retry errors to avoid breaking blocking flow.\n }\n }\n};\n\n/**\n * Blocks navigation in TanStack Router applications based on conditions or scope state.\n *\n * This hook integrates with TanStack Router's history blocking mechanism and\n * react-action-guard's scope system to prevent navigation when certain conditions\n * are met or when specific scopes are blocked.\n *\n * **Features:**\n * - Block navigation based on boolean condition or function\n * - Automatically block when react-action-guard scopes are active\n * - Support for browser `beforeunload` event (tab close/refresh)\n * - Customizable confirmation dialog\n * - Callbacks for block/allow events\n *\n * @param options - Configuration options\n * @returns Object with `isBlocking` state\n *\n * @example\n * ```tsx\n * // Block based on scope\n * function EditForm() {\n * useNavigationBlocker({\n * scope: 'form',\n * message: 'You have unsaved changes. Leave anyway?',\n * });\n *\n * return <form>...</form>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Block based on condition\n * function UnsavedChanges() {\n * const [hasChanges, setHasChanges] = useState(false);\n *\n * useNavigationBlocker({\n * when: hasChanges,\n * message: 'Discard changes?',\n * onBlock: () => console.log('Navigation blocked'),\n * });\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Type-safe with scopes\n * import { createTypedHooks } from '@okyrychenko-dev/react-action-guard';\n *\n * type AppScopes = 'global' | 'form' | 'checkout';\n * const { useBlocker } = createTypedHooks<AppScopes>();\n *\n * function MyComponent() {\n * useBlocker('blocker-id', { scope: 'form' });\n *\n * useNavigationBlocker({\n * scope: 'form', // Type-safe!\n * message: 'Leave?',\n * });\n * }\n * ```\n */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const { when, scope, message, onBlock, onAllow, blockBrowserUnload = true, onConfirm } = options;\n\n const router = useRouter();\n\n // Use shared logic to determine if blocking should be active\n const shouldBlock = useShouldBlock(when, scope);\n\n // Block navigation using TanStack Router's history\n useEffect(() => {\n if (!shouldBlock || !hasBlockingHistory(router)) {\n return;\n }\n\n // Block navigation with properly typed update parameter\n type NavigationUpdate = Record<string, unknown> & { retry?: () => void };\n const unblock = router.history.block((update: NavigationUpdate) => {\n // Early return if no message\n if (!message) {\n return;\n }\n\n // Trigger onBlock callback\n onBlock?.();\n\n const confirmation = resolveConfirmResult(message, onConfirm, (value) =>\n window.confirm(value)\n );\n\n if (confirmation.kind === \"async\") {\n confirmation.promise\n .then((confirmed) => {\n if (confirmed) {\n onAllow?.();\n unblock();\n attemptRetry(update);\n }\n })\n .catch((error: unknown) => {\n // On error, unblock but don't retry\n unblock();\n if (process.env.NODE_ENV !== \"production\") {\n console.error(\"[react-action-guard-router] Confirmation error:\", error);\n }\n });\n return;\n }\n\n if (confirmation.confirmed) {\n onAllow?.();\n unblock();\n attemptRetry(update);\n }\n });\n\n return () => {\n unblock();\n };\n }, [shouldBlock, message, onBlock, onAllow, router, onConfirm]);\n\n // Also block browser unload if requested\n useBeforeUnload(blockBrowserUnload && shouldBlock, message ?? DEFAULT_UNLOAD_MESSAGE);\n\n return {\n isBlocking: shouldBlock,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/utils.ts","../../src/core/useShouldBlock.ts","../../src/core/confirmationOwner.utils.ts","../../src/tanstack-router/useNavigationBlocker.ts"],"names":["isFunction","isObject","isArray","useMemo","useIsBlocked","useRef","useEffect","isDefined","useState","useCallback","useBlocker"],"mappings":"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","file":"index.cjs","sourcesContent":["import { isArray, isFunction, isObject } from \"@okyrychenko-dev/type-utils\";\nimport type { Optional } from \"@okyrychenko-dev/type-utils\";\n\n/**\n * Resolves a condition value (boolean or function returning boolean)\n *\n * @param condition - Boolean value or function\n * @returns Resolved boolean value\n */\nexport function resolveCondition(condition: boolean | (() => boolean)): boolean {\n return isFunction(condition) ? condition() : condition;\n}\n\ninterface Thenable {\n then: (...args: Array<unknown>) => unknown;\n}\n\nconst hasThen = (value: unknown): value is Thenable => isObject(value) && \"then\" in value;\n\n/**\n * Checks if a value behaves like a Promise (thenable)\n */\nexport function isThenable<T>(value: unknown): value is PromiseLike<T> {\n if (!hasThen(value)) {\n return false;\n }\n\n return isFunction(value.then);\n}\n\nexport interface ConfirmResultSync {\n kind: \"sync\";\n confirmed: boolean;\n}\n\nexport interface ConfirmResultAsync {\n kind: \"async\";\n promise: Promise<boolean>;\n}\n\nexport type ConfirmResult = ConfirmResultSync | ConfirmResultAsync;\n\n/**\n * Normalizes custom confirm result into sync or async branch.\n */\nexport function resolveConfirmResult(\n message: string,\n onConfirm: Optional<(message: string) => boolean | PromiseLike<boolean>>,\n fallbackConfirm: (message: string) => boolean\n): ConfirmResult {\n if (!onConfirm) {\n return { kind: \"sync\", confirmed: fallbackConfirm(message) };\n }\n\n const result = onConfirm(message);\n\n if (isThenable<boolean>(result)) {\n return { kind: \"async\", promise: Promise.resolve(result) };\n }\n\n return { kind: \"sync\", confirmed: result };\n}\n\n/**\n * Generates a unique blocker ID based on scope and timestamp\n *\n * @param prefix - Prefix for the ID\n * @param scope - Optional scope\n * @returns Unique blocker ID\n */\nexport function createBlockerId(prefix: string, scope?: string | Array<string>): string {\n const scopePart = scope ? `-${isArray(scope) ? scope.join(\"-\") : scope}` : \"\";\n const timestamp = String(Date.now());\n\n return `${prefix}${scopePart}-${timestamp}`;\n}\n\n/**\n * Checks if a value is defined and not empty\n *\n * @param value - Value to check\n * @returns True if value is defined and not empty\n */\nexport { isDefined } from \"@okyrychenko-dev/type-utils\";\n\n/**\n * Normalizes scope to an array\n *\n * @param scope - String or array of strings\n * @returns Array of scope strings\n */\nexport function normalizeScope(scope: Optional<string | Array<string>>): Array<string> {\n if (!scope) {\n return [];\n }\n\n return isArray(scope) ? scope : [scope];\n}\n\n/** Identifies protection independently of scope order and duplicate entries. */\nexport function createScopeKey(scope: Optional<string | Array<string>>): string {\n return JSON.stringify([...new Set(normalizeScope(scope))].sort());\n}\n","import { useIsBlocked } from \"@okyrychenko-dev/react-action-guard\";\nimport { isDefined } from \"@okyrychenko-dev/type-utils\";\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { normalizeScope, resolveCondition } from \"./utils\";\n\n/**\n * Shared hook to determine if blocking should be active based on\n * condition and/or scope state.\n *\n * Pure function conditions are evaluated synchronously on every render.\n * Mutating a ref alone does not trigger a render.\n *\n * @param when - Optional condition (boolean or function)\n * @param scope - Optional scope(s) to check\n * @returns Whether blocking should be active\n *\n * @internal\n */\nexport function useShouldBlock(\n when?: boolean | (() => boolean),\n scope?: string | Array<string>\n): boolean {\n // Memoize scope normalization to avoid recalculation on every render\n const scopes = useMemo(() => normalizeScope(scope), [scope]);\n\n // Check if any specified scopes are blocked\n const isScopeBlocked = useIsBlocked(scopes.length === 1 ? scopes[0] : scopes);\n\n // Development-only validation\n const warnedMissingConditionRef = useRef(false);\n\n useEffect(() => {\n if (process.env.NODE_ENV !== \"development\") {\n return;\n }\n\n if (!warnedMissingConditionRef.current && !isDefined(when) && !scope) {\n warnedMissingConditionRef.current = true;\n console.warn(\n '[react-action-guard-router] useShouldBlock: Neither \"when\" nor \"scope\" provided. ' +\n \"This hook will always return false. Please provide at least one condition.\"\n );\n }\n }, [when, scope]);\n\n const blockCondition = isDefined(when) && resolveCondition(when);\n\n return blockCondition || Boolean(scope && isScopeBlocked);\n}\n","import type { ConfirmationOwner } from \"./confirmationOwner.types\";\n\nexport function createConfirmationOwner(): ConfirmationOwner {\n let currentAttempt = 0;\n\n function invalidate(): void {\n currentAttempt += 1;\n }\n\n function begin(): () => boolean {\n const attempt = ++currentAttempt;\n\n function settle(): boolean {\n if (attempt !== currentAttempt) {\n return false;\n }\n invalidate();\n\n return true;\n }\n\n return settle;\n }\n\n return { begin, invalidate };\n}\n","import { useBlocker } from \"@tanstack/react-router\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport {\n createConfirmationOwner,\n createScopeKey,\n resolveConfirmResult,\n useShouldBlock,\n} from \"../core\";\nimport type { NavigationBlockerReturn } from \"../core\";\nimport type { UseNavigationBlockerOptions } from \"./types\";\n\n/** Blocks native TanStack transitions while retaining protection after confirmation. */\nexport function useNavigationBlocker(\n options: UseNavigationBlockerOptions\n): NavigationBlockerReturn {\n const { when, scope, message, onBlock, onAllow, blockBrowserUnload = true, onConfirm } = options;\n\n const shouldBlock = useShouldBlock(when, scope);\n const scopeKey = createScopeKey(scope);\n const [confirmationOwner] = useState(createConfirmationOwner);\n\n const { begin, invalidate } = confirmationOwner;\n\n useEffect(() => {\n return invalidate;\n }, [when, scopeKey, shouldBlock, message, onConfirm, invalidate]);\n\n const shouldBlockFn = useCallback(async () => {\n const settle = begin();\n\n onBlock?.();\n\n if (!message) {\n return true;\n }\n\n try {\n const confirmation = resolveConfirmResult(message, onConfirm, (value) =>\n window.confirm(value)\n );\n let confirmed: boolean;\n\n if (confirmation.kind === \"async\") {\n confirmed = await confirmation.promise;\n } else {\n confirmed = confirmation.confirmed;\n }\n\n if (!settle() || !confirmed) {\n return true;\n }\n\n onAllow?.();\n\n return false;\n } catch {\n return true;\n }\n }, [message, onBlock, onAllow, onConfirm, begin]);\n\n useBlocker({\n shouldBlockFn,\n enableBeforeUnload: blockBrowserUnload && shouldBlock,\n disabled: !shouldBlock,\n });\n\n return { isBlocking: shouldBlock };\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as BaseNavigationBlockerOptions, N as NavigationBlockerReturn } from '../types-
|
|
1
|
+
import { B as BaseNavigationBlockerOptions, N as NavigationBlockerReturn } from '../types-xxRwsJ1N.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Options for TanStack Router navigation blocker
|
|
@@ -8,68 +8,7 @@ import { B as BaseNavigationBlockerOptions, N as NavigationBlockerReturn } from
|
|
|
8
8
|
*/
|
|
9
9
|
type UseNavigationBlockerOptions = BaseNavigationBlockerOptions;
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Blocks navigation in TanStack Router applications based on conditions or scope state.
|
|
13
|
-
*
|
|
14
|
-
* This hook integrates with TanStack Router's history blocking mechanism and
|
|
15
|
-
* react-action-guard's scope system to prevent navigation when certain conditions
|
|
16
|
-
* are met or when specific scopes are blocked.
|
|
17
|
-
*
|
|
18
|
-
* **Features:**
|
|
19
|
-
* - Block navigation based on boolean condition or function
|
|
20
|
-
* - Automatically block when react-action-guard scopes are active
|
|
21
|
-
* - Support for browser `beforeunload` event (tab close/refresh)
|
|
22
|
-
* - Customizable confirmation dialog
|
|
23
|
-
* - Callbacks for block/allow events
|
|
24
|
-
*
|
|
25
|
-
* @param options - Configuration options
|
|
26
|
-
* @returns Object with `isBlocking` state
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```tsx
|
|
30
|
-
* // Block based on scope
|
|
31
|
-
* function EditForm() {
|
|
32
|
-
* useNavigationBlocker({
|
|
33
|
-
* scope: 'form',
|
|
34
|
-
* message: 'You have unsaved changes. Leave anyway?',
|
|
35
|
-
* });
|
|
36
|
-
*
|
|
37
|
-
* return <form>...</form>;
|
|
38
|
-
* }
|
|
39
|
-
* ```
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* ```tsx
|
|
43
|
-
* // Block based on condition
|
|
44
|
-
* function UnsavedChanges() {
|
|
45
|
-
* const [hasChanges, setHasChanges] = useState(false);
|
|
46
|
-
*
|
|
47
|
-
* useNavigationBlocker({
|
|
48
|
-
* when: hasChanges,
|
|
49
|
-
* message: 'Discard changes?',
|
|
50
|
-
* onBlock: () => console.log('Navigation blocked'),
|
|
51
|
-
* });
|
|
52
|
-
* }
|
|
53
|
-
* ```
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* ```tsx
|
|
57
|
-
* // Type-safe with scopes
|
|
58
|
-
* import { createTypedHooks } from '@okyrychenko-dev/react-action-guard';
|
|
59
|
-
*
|
|
60
|
-
* type AppScopes = 'global' | 'form' | 'checkout';
|
|
61
|
-
* const { useBlocker } = createTypedHooks<AppScopes>();
|
|
62
|
-
*
|
|
63
|
-
* function MyComponent() {
|
|
64
|
-
* useBlocker('blocker-id', { scope: 'form' });
|
|
65
|
-
*
|
|
66
|
-
* useNavigationBlocker({
|
|
67
|
-
* scope: 'form', // Type-safe!
|
|
68
|
-
* message: 'Leave?',
|
|
69
|
-
* });
|
|
70
|
-
* }
|
|
71
|
-
* ```
|
|
72
|
-
*/
|
|
11
|
+
/** Blocks native TanStack transitions while retaining protection after confirmation. */
|
|
73
12
|
declare function useNavigationBlocker(options: UseNavigationBlockerOptions): NavigationBlockerReturn;
|
|
74
13
|
|
|
75
14
|
export { type UseNavigationBlockerOptions, useNavigationBlocker };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as BaseNavigationBlockerOptions, N as NavigationBlockerReturn } from '../types-
|
|
1
|
+
import { B as BaseNavigationBlockerOptions, N as NavigationBlockerReturn } from '../types-xxRwsJ1N.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Options for TanStack Router navigation blocker
|
|
@@ -8,68 +8,7 @@ import { B as BaseNavigationBlockerOptions, N as NavigationBlockerReturn } from
|
|
|
8
8
|
*/
|
|
9
9
|
type UseNavigationBlockerOptions = BaseNavigationBlockerOptions;
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Blocks navigation in TanStack Router applications based on conditions or scope state.
|
|
13
|
-
*
|
|
14
|
-
* This hook integrates with TanStack Router's history blocking mechanism and
|
|
15
|
-
* react-action-guard's scope system to prevent navigation when certain conditions
|
|
16
|
-
* are met or when specific scopes are blocked.
|
|
17
|
-
*
|
|
18
|
-
* **Features:**
|
|
19
|
-
* - Block navigation based on boolean condition or function
|
|
20
|
-
* - Automatically block when react-action-guard scopes are active
|
|
21
|
-
* - Support for browser `beforeunload` event (tab close/refresh)
|
|
22
|
-
* - Customizable confirmation dialog
|
|
23
|
-
* - Callbacks for block/allow events
|
|
24
|
-
*
|
|
25
|
-
* @param options - Configuration options
|
|
26
|
-
* @returns Object with `isBlocking` state
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```tsx
|
|
30
|
-
* // Block based on scope
|
|
31
|
-
* function EditForm() {
|
|
32
|
-
* useNavigationBlocker({
|
|
33
|
-
* scope: 'form',
|
|
34
|
-
* message: 'You have unsaved changes. Leave anyway?',
|
|
35
|
-
* });
|
|
36
|
-
*
|
|
37
|
-
* return <form>...</form>;
|
|
38
|
-
* }
|
|
39
|
-
* ```
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* ```tsx
|
|
43
|
-
* // Block based on condition
|
|
44
|
-
* function UnsavedChanges() {
|
|
45
|
-
* const [hasChanges, setHasChanges] = useState(false);
|
|
46
|
-
*
|
|
47
|
-
* useNavigationBlocker({
|
|
48
|
-
* when: hasChanges,
|
|
49
|
-
* message: 'Discard changes?',
|
|
50
|
-
* onBlock: () => console.log('Navigation blocked'),
|
|
51
|
-
* });
|
|
52
|
-
* }
|
|
53
|
-
* ```
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* ```tsx
|
|
57
|
-
* // Type-safe with scopes
|
|
58
|
-
* import { createTypedHooks } from '@okyrychenko-dev/react-action-guard';
|
|
59
|
-
*
|
|
60
|
-
* type AppScopes = 'global' | 'form' | 'checkout';
|
|
61
|
-
* const { useBlocker } = createTypedHooks<AppScopes>();
|
|
62
|
-
*
|
|
63
|
-
* function MyComponent() {
|
|
64
|
-
* useBlocker('blocker-id', { scope: 'form' });
|
|
65
|
-
*
|
|
66
|
-
* useNavigationBlocker({
|
|
67
|
-
* scope: 'form', // Type-safe!
|
|
68
|
-
* message: 'Leave?',
|
|
69
|
-
* });
|
|
70
|
-
* }
|
|
71
|
-
* ```
|
|
72
|
-
*/
|
|
11
|
+
/** Blocks native TanStack transitions while retaining protection after confirmation. */
|
|
73
12
|
declare function useNavigationBlocker(options: UseNavigationBlockerOptions): NavigationBlockerReturn;
|
|
74
13
|
|
|
75
14
|
export { type UseNavigationBlockerOptions, useNavigationBlocker };
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useEffect, useMemo, useRef } from 'react';
|
|
1
|
+
import { useBlocker } from '@tanstack/react-router';
|
|
2
|
+
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
3
|
+
import { isDefined, isArray, isFunction, isObject } from '@okyrychenko-dev/type-utils';
|
|
3
4
|
import { useIsBlocked } from '@okyrychenko-dev/react-action-guard';
|
|
4
5
|
|
|
5
6
|
// src/tanstack-router/useNavigationBlocker.ts
|
|
6
|
-
|
|
7
|
-
// src/core/utils.ts
|
|
8
7
|
function resolveCondition(condition) {
|
|
9
|
-
return
|
|
8
|
+
return isFunction(condition) ? condition() : condition;
|
|
10
9
|
}
|
|
11
|
-
var hasThen = (value) =>
|
|
10
|
+
var hasThen = (value) => isObject(value) && "then" in value;
|
|
12
11
|
function isThenable(value) {
|
|
13
12
|
if (!hasThen(value)) {
|
|
14
13
|
return false;
|
|
15
14
|
}
|
|
16
|
-
return
|
|
15
|
+
return isFunction(value.then);
|
|
17
16
|
}
|
|
18
17
|
function resolveConfirmResult(message, onConfirm, fallbackConfirm) {
|
|
19
18
|
if (!onConfirm) {
|
|
@@ -25,41 +24,19 @@ function resolveConfirmResult(message, onConfirm, fallbackConfirm) {
|
|
|
25
24
|
}
|
|
26
25
|
return { kind: "sync", confirmed: result };
|
|
27
26
|
}
|
|
28
|
-
function isDefined(value) {
|
|
29
|
-
return value !== void 0 && value !== null;
|
|
30
|
-
}
|
|
31
27
|
function normalizeScope(scope) {
|
|
32
28
|
if (!scope) {
|
|
33
29
|
return [];
|
|
34
30
|
}
|
|
35
|
-
return
|
|
31
|
+
return isArray(scope) ? scope : [scope];
|
|
36
32
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
var DEFAULT_UNLOAD_MESSAGE = "Changes you made may not be saved.";
|
|
40
|
-
function useBeforeUnload(when, message = DEFAULT_UNLOAD_MESSAGE) {
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
const shouldBlock = resolveCondition(when);
|
|
43
|
-
if (!shouldBlock) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
const handleBeforeUnload = (event) => {
|
|
47
|
-
event.preventDefault();
|
|
48
|
-
if ("returnValue" in event) {
|
|
49
|
-
event.returnValue = message;
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
53
|
-
return () => {
|
|
54
|
-
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
55
|
-
};
|
|
56
|
-
}, [when, message]);
|
|
33
|
+
function createScopeKey(scope) {
|
|
34
|
+
return JSON.stringify([...new Set(normalizeScope(scope))].sort());
|
|
57
35
|
}
|
|
58
36
|
function useShouldBlock(when, scope) {
|
|
59
37
|
const scopes = useMemo(() => normalizeScope(scope), [scope]);
|
|
60
38
|
const isScopeBlocked = useIsBlocked(scopes.length === 1 ? scopes[0] : scopes);
|
|
61
39
|
const warnedMissingConditionRef = useRef(false);
|
|
62
|
-
const warnedFunctionRef = useRef(false);
|
|
63
40
|
useEffect(() => {
|
|
64
41
|
if (process.env.NODE_ENV !== "development") {
|
|
65
42
|
return;
|
|
@@ -70,99 +47,74 @@ function useShouldBlock(when, scope) {
|
|
|
70
47
|
'[react-action-guard-router] useShouldBlock: Neither "when" nor "scope" provided. This hook will always return false. Please provide at least one condition.'
|
|
71
48
|
);
|
|
72
49
|
}
|
|
73
|
-
if (!warnedFunctionRef.current && typeof when === "function") {
|
|
74
|
-
warnedFunctionRef.current = true;
|
|
75
|
-
console.warn(
|
|
76
|
-
'[react-action-guard-router] useShouldBlock: "when" is a function. Make sure to wrap it with useCallback to avoid unnecessary re-computations.'
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
50
|
}, [when, scope]);
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
if (isDefined(when)) {
|
|
83
|
-
blockCondition = resolveCondition(when);
|
|
84
|
-
}
|
|
85
|
-
if (scope && isScopeBlocked) {
|
|
86
|
-
blockCondition = true;
|
|
87
|
-
}
|
|
88
|
-
return blockCondition;
|
|
89
|
-
}, [when, scope, isScopeBlocked]);
|
|
90
|
-
return shouldBlock;
|
|
51
|
+
const blockCondition = isDefined(when) && resolveCondition(when);
|
|
52
|
+
return blockCondition || Boolean(scope && isScopeBlocked);
|
|
91
53
|
}
|
|
92
54
|
|
|
93
|
-
// src/
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!("history" in value)) {
|
|
99
|
-
return false;
|
|
55
|
+
// src/core/confirmationOwner.utils.ts
|
|
56
|
+
function createConfirmationOwner() {
|
|
57
|
+
let currentAttempt = 0;
|
|
58
|
+
function invalidate() {
|
|
59
|
+
currentAttempt += 1;
|
|
100
60
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
};
|
|
110
|
-
var hasRetry = (value) => typeof value.retry === "function";
|
|
111
|
-
var attemptRetry = (update) => {
|
|
112
|
-
if (hasRetry(update)) {
|
|
113
|
-
try {
|
|
114
|
-
update.retry();
|
|
115
|
-
} catch {
|
|
61
|
+
function begin() {
|
|
62
|
+
const attempt = ++currentAttempt;
|
|
63
|
+
function settle() {
|
|
64
|
+
if (attempt !== currentAttempt) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
invalidate();
|
|
68
|
+
return true;
|
|
116
69
|
}
|
|
70
|
+
return settle;
|
|
117
71
|
}
|
|
118
|
-
};
|
|
72
|
+
return { begin, invalidate };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/tanstack-router/useNavigationBlocker.ts
|
|
119
76
|
function useNavigationBlocker(options) {
|
|
120
77
|
const { when, scope, message, onBlock, onAllow, blockBrowserUnload = true, onConfirm } = options;
|
|
121
|
-
const router = useRouter();
|
|
122
78
|
const shouldBlock = useShouldBlock(when, scope);
|
|
79
|
+
const scopeKey = createScopeKey(scope);
|
|
80
|
+
const [confirmationOwner] = useState(createConfirmationOwner);
|
|
81
|
+
const { begin, invalidate } = confirmationOwner;
|
|
123
82
|
useEffect(() => {
|
|
124
|
-
|
|
125
|
-
|
|
83
|
+
return invalidate;
|
|
84
|
+
}, [when, scopeKey, shouldBlock, message, onConfirm, invalidate]);
|
|
85
|
+
const shouldBlockFn = useCallback(async () => {
|
|
86
|
+
const settle = begin();
|
|
87
|
+
onBlock?.();
|
|
88
|
+
if (!message) {
|
|
89
|
+
return true;
|
|
126
90
|
}
|
|
127
|
-
|
|
128
|
-
if (!message) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
onBlock?.();
|
|
91
|
+
try {
|
|
132
92
|
const confirmation = resolveConfirmResult(
|
|
133
93
|
message,
|
|
134
94
|
onConfirm,
|
|
135
95
|
(value) => window.confirm(value)
|
|
136
96
|
);
|
|
97
|
+
let confirmed;
|
|
137
98
|
if (confirmation.kind === "async") {
|
|
138
|
-
confirmation.promise
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
unblock();
|
|
142
|
-
attemptRetry(update);
|
|
143
|
-
}
|
|
144
|
-
}).catch((error) => {
|
|
145
|
-
unblock();
|
|
146
|
-
if (process.env.NODE_ENV !== "production") {
|
|
147
|
-
console.error("[react-action-guard-router] Confirmation error:", error);
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
return;
|
|
99
|
+
confirmed = await confirmation.promise;
|
|
100
|
+
} else {
|
|
101
|
+
confirmed = confirmation.confirmed;
|
|
151
102
|
}
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
unblock();
|
|
155
|
-
attemptRetry(update);
|
|
103
|
+
if (!settle() || !confirmed) {
|
|
104
|
+
return true;
|
|
156
105
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
106
|
+
onAllow?.();
|
|
107
|
+
return false;
|
|
108
|
+
} catch {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}, [message, onBlock, onAllow, onConfirm, begin]);
|
|
112
|
+
useBlocker({
|
|
113
|
+
shouldBlockFn,
|
|
114
|
+
enableBeforeUnload: blockBrowserUnload && shouldBlock,
|
|
115
|
+
disabled: !shouldBlock
|
|
116
|
+
});
|
|
117
|
+
return { isBlocking: shouldBlock };
|
|
166
118
|
}
|
|
167
119
|
|
|
168
120
|
export { useNavigationBlocker };
|