next-unsaved-changes-guard 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/README.md +133 -0
- package/dist/index.cjs +245 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +218 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Next Unsaved Changes Guard
|
|
2
|
+
|
|
3
|
+
A lightweight, customizable React component for Next.js 13+ (App Router) that prevents users from accidentally leaving a page with unsaved changes.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🛡️ **Route Guard**: Intercepts navigation attempts when determining that a form is dirty.
|
|
8
|
+
- 🔄 **Browser Event Handling**: Warns users when closing the tab or refreshing the window.
|
|
9
|
+
- 🎨 **Fully Customizable**: Override default styles easily using stable CSS class names.
|
|
10
|
+
- 🧩 **Next.js App Router Support**: Built specifically for `next/navigation`.
|
|
11
|
+
|
|
12
|
+
## Compatibility
|
|
13
|
+
|
|
14
|
+
| Package | Version |
|
|
15
|
+
| ------- | ------------------------------- |
|
|
16
|
+
| Next.js | `13.0.0` or higher (App Router) |
|
|
17
|
+
| React | `18.0.0` or higher |
|
|
18
|
+
|
|
19
|
+
> [!NOTE]
|
|
20
|
+
> This package utilizes `next/navigation` which is exclusive to the **Next.js App Router**. It is **not** compatible with the legacy Pages Router (`pages/` directory).
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install next-unsaved-changes-guard
|
|
26
|
+
# or
|
|
27
|
+
yarn add next-unsaved-changes-guard
|
|
28
|
+
# or
|
|
29
|
+
pnpm add next-unsaved-changes-guard
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
Import the component and wrap your form or page content. Pass the `formDirty` state to control when the guard should be active.
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
"use client";
|
|
38
|
+
|
|
39
|
+
import { useEffect, useRef, useState } from "react";
|
|
40
|
+
import { NextUnsavedChangesGuard } from "next-unsaved-changes-guard";
|
|
41
|
+
|
|
42
|
+
export default function MyFormPage() {
|
|
43
|
+
const [isDirty, setIsDirty] = useState(false);
|
|
44
|
+
const [errors, setErrors] = useState({});
|
|
45
|
+
const submitBtnRef = useRef<HTMLButtonElement>(null);
|
|
46
|
+
|
|
47
|
+
// Example save function
|
|
48
|
+
const handleSave = async () => {
|
|
49
|
+
// Perform API call or form submission
|
|
50
|
+
console.log("Saving data...");
|
|
51
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<NextUnsavedChangesGuard
|
|
56
|
+
formDirty={isDirty}
|
|
57
|
+
formErrors={errors}
|
|
58
|
+
saveData={() => submitBtnRef.current?.click()}
|
|
59
|
+
>
|
|
60
|
+
<form
|
|
61
|
+
onChange={() => setIsDirty(true)}
|
|
62
|
+
onSubmit={(e) => {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
handleSave();
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<h1>Edit Profile</h1>
|
|
68
|
+
<input type="text" placeholder="Name" />
|
|
69
|
+
|
|
70
|
+
{/* Hidden submit button triggered by the Guard's "Save" action */}
|
|
71
|
+
<button ref={submitBtnRef} type="submit" style={{ display: "none" }} />
|
|
72
|
+
</form>
|
|
73
|
+
</NextUnsavedChangesGuard>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Props
|
|
79
|
+
|
|
80
|
+
| Prop | Type | Default | Description |
|
|
81
|
+
| ------------ | --------------------- | ------------ | ---------------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| `children` | `ReactNode` | **Required** | The content to be rendered (usually your page or form). |
|
|
83
|
+
| `formDirty` | `boolean` | **Required** | If `true`, navigation attempts will trigger the confirmation dialog. |
|
|
84
|
+
| `saveData` | `() => Promise<void>` | **Required** | Function called when the user clicks "Save" in the dialog. |
|
|
85
|
+
| `formErrors` | `Record<string, any>` | `{}` | If provided and not empty, the dialog will prevent navigation on "Save" attempt until errors are resolved. |
|
|
86
|
+
|
|
87
|
+
## Styling
|
|
88
|
+
|
|
89
|
+
The component injects default styles, but every element has a stable CSS class name that you can target in your global CSS or CSS modules to override the look and feel.
|
|
90
|
+
|
|
91
|
+
### CSS Classes
|
|
92
|
+
|
|
93
|
+
| Class Name | Description |
|
|
94
|
+
| ---------------------- | -------------------------------------------------- |
|
|
95
|
+
| `.nucg-ad-overlay` | The semi-transparent backdrop covering the screen. |
|
|
96
|
+
| `.nucg-ad-dialog` | The main modal container. |
|
|
97
|
+
| `.nucg-ad-title` | The modal header title ("Unsaved Changes"). |
|
|
98
|
+
| `.nucg-ad-description` | The warning message text. |
|
|
99
|
+
| `.nucg-ad-footer` | Container for the action buttons. |
|
|
100
|
+
| `.nucg-ad-btn` | Base style shared by all buttons. |
|
|
101
|
+
| `.nucg-ad-cancel` | The "Stay" button (Secondary). |
|
|
102
|
+
| `.nucg-ad-danger` | The "Discard" button (Destructive). |
|
|
103
|
+
| `.nucg-ad-primary` | The "Save" button (Primary). |
|
|
104
|
+
|
|
105
|
+
### Example Customization
|
|
106
|
+
|
|
107
|
+
Add this to your `globals.css` to match your brand colors:
|
|
108
|
+
|
|
109
|
+
```css
|
|
110
|
+
/* Override the Save button color */
|
|
111
|
+
.nucg-ad-primary {
|
|
112
|
+
background-color: #3b82f6 !important; /* Tailwind Blue-500 */
|
|
113
|
+
}
|
|
114
|
+
.nucg-ad-primary:hover {
|
|
115
|
+
background-color: #2563eb !important; /* Tailwind Blue-600 */
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Customize the modal radius and shadow */
|
|
119
|
+
.nucg-ad-dialog {
|
|
120
|
+
border-radius: 16px !important;
|
|
121
|
+
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1) !important;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Change the backdrop color */
|
|
125
|
+
.nucg-ad-overlay {
|
|
126
|
+
background: rgba(0, 0, 0, 0.7) !important;
|
|
127
|
+
backdrop-filter: blur(4px);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT © Shibu Dhara
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.tsx
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
NextUnsavedChangesGuard: () => NextUnsavedChangesGuard
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/NextUnsavedChangesGuard.tsx
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
var import_navigation = require("next/navigation");
|
|
30
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
31
|
+
var alertDialogCSS = `
|
|
32
|
+
.nucg-ad-overlay {
|
|
33
|
+
position: fixed;
|
|
34
|
+
inset: 0;
|
|
35
|
+
background: rgba(0,0,0,0.45);
|
|
36
|
+
backdrop-filter: blur(2px);
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: center;
|
|
40
|
+
z-index: 50;
|
|
41
|
+
animation: ad-fade-in .15s ease-out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.nucg-ad-dialog {
|
|
45
|
+
width: 100%;
|
|
46
|
+
max-width: 420px;
|
|
47
|
+
background: #ffffff;
|
|
48
|
+
border-radius: 12px;
|
|
49
|
+
padding: 24px;
|
|
50
|
+
box-shadow:
|
|
51
|
+
0 10px 30px rgba(0,0,0,0.15),
|
|
52
|
+
0 1px 2px rgba(0,0,0,0.1);
|
|
53
|
+
animation: ad-scale-in .15s ease-out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.nucg-ad-title {
|
|
57
|
+
font-size: 1.125rem;
|
|
58
|
+
font-weight: 600;
|
|
59
|
+
color: #111827;
|
|
60
|
+
margin-bottom: 6px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.nucg-ad-description {
|
|
64
|
+
font-size: 0.95rem;
|
|
65
|
+
color: #6b7280;
|
|
66
|
+
line-height: 1.45;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.nucg-ad-footer {
|
|
70
|
+
display: flex;
|
|
71
|
+
justify-content: flex-end;
|
|
72
|
+
gap: 10px;
|
|
73
|
+
margin-top: 20px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.nucg-ad-btn {
|
|
77
|
+
padding: 8px 14px;
|
|
78
|
+
border-radius: 8px;
|
|
79
|
+
font-size: 0.875rem;
|
|
80
|
+
border: none;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
transition: background .15s ease, transform .05s ease;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.nucg-ad-btn:active {
|
|
86
|
+
transform: scale(0.97);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.nucg-ad-cancel {
|
|
90
|
+
background: #f3f4f6;
|
|
91
|
+
color: #111827;
|
|
92
|
+
}
|
|
93
|
+
.nucg-ad-cancel:hover {
|
|
94
|
+
background: #e5e7eb;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.nucg-ad-danger {
|
|
98
|
+
background: #fee2e2;
|
|
99
|
+
color: #b91c1c;
|
|
100
|
+
}
|
|
101
|
+
.nucg-ad-danger:hover {
|
|
102
|
+
background: #fecaca;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.nucg-ad-primary {
|
|
106
|
+
background: #111827;
|
|
107
|
+
color: #ffffff;
|
|
108
|
+
}
|
|
109
|
+
.nucg-ad-primary:hover {
|
|
110
|
+
background: #1f2937;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@keyframes nucg-ad-fade-in {
|
|
114
|
+
from { opacity: 0; }
|
|
115
|
+
to { opacity: 1; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@keyframes nucg-ad-scale-in {
|
|
119
|
+
from { transform: scale(.96); opacity: 0; }
|
|
120
|
+
to { transform: scale(1); opacity: 1; }
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
var NextUnsavedChangesGuard = ({
|
|
124
|
+
children,
|
|
125
|
+
formDirty,
|
|
126
|
+
saveData,
|
|
127
|
+
formErrors = {}
|
|
128
|
+
}) => {
|
|
129
|
+
const [isDirty, setDirty] = (0, import_react.useState)(formDirty);
|
|
130
|
+
const [isDialogOpen, setIsDialogOpen] = (0, import_react.useState)(false);
|
|
131
|
+
const [nextRoute, setNextRoute] = (0, import_react.useState)(null);
|
|
132
|
+
const router = (0, import_navigation.useRouter)();
|
|
133
|
+
const isRouteChanging = (0, import_react.useRef)(false);
|
|
134
|
+
(0, import_react.useEffect)(() => {
|
|
135
|
+
setDirty(formDirty);
|
|
136
|
+
}, [formDirty]);
|
|
137
|
+
(0, import_react.useEffect)(() => {
|
|
138
|
+
const handleWindowClose = (e) => {
|
|
139
|
+
if (!isDirty) return;
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
e.returnValue = "You have unsaved changes. Are you sure you want to leave?";
|
|
142
|
+
};
|
|
143
|
+
window.addEventListener("beforeunload", handleWindowClose);
|
|
144
|
+
return () => {
|
|
145
|
+
window.removeEventListener("beforeunload", handleWindowClose);
|
|
146
|
+
};
|
|
147
|
+
}, [isDirty]);
|
|
148
|
+
(0, import_react.useEffect)(() => {
|
|
149
|
+
const originalPush = router.push;
|
|
150
|
+
router.push = (url) => {
|
|
151
|
+
if (isDirty && !isRouteChanging.current) {
|
|
152
|
+
setNextRoute(url);
|
|
153
|
+
setIsDialogOpen(true);
|
|
154
|
+
return Promise.resolve();
|
|
155
|
+
}
|
|
156
|
+
isRouteChanging.current = false;
|
|
157
|
+
return originalPush(url);
|
|
158
|
+
};
|
|
159
|
+
return () => {
|
|
160
|
+
router.push = originalPush;
|
|
161
|
+
};
|
|
162
|
+
}, [isDirty, router]);
|
|
163
|
+
const handleSave = async () => {
|
|
164
|
+
if (Object.keys(formErrors).length === 0) {
|
|
165
|
+
try {
|
|
166
|
+
await saveData();
|
|
167
|
+
setDirty(false);
|
|
168
|
+
setIsDialogOpen(false);
|
|
169
|
+
if (nextRoute) {
|
|
170
|
+
isRouteChanging.current = true;
|
|
171
|
+
router.push(nextRoute);
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.log("Error saving changes");
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
setIsDialogOpen(false);
|
|
178
|
+
console.log("Form contains errors");
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const handleDiscard = () => {
|
|
182
|
+
setDirty(false);
|
|
183
|
+
setIsDialogOpen(false);
|
|
184
|
+
if (nextRoute) {
|
|
185
|
+
isRouteChanging.current = true;
|
|
186
|
+
router.push(nextRoute);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const handleStay = () => {
|
|
190
|
+
setIsDialogOpen(false);
|
|
191
|
+
setNextRoute(null);
|
|
192
|
+
};
|
|
193
|
+
function injectAlertDialogStyles() {
|
|
194
|
+
if (typeof document === "undefined") return;
|
|
195
|
+
if (!document.getElementById("alert-dialog-styles")) {
|
|
196
|
+
const style = document.createElement("style");
|
|
197
|
+
style.id = "alert-dialog-styles";
|
|
198
|
+
style.textContent = alertDialogCSS;
|
|
199
|
+
document.head.appendChild(style);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
(0, import_react.useEffect)(() => {
|
|
203
|
+
injectAlertDialogStyles();
|
|
204
|
+
}, []);
|
|
205
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
206
|
+
children,
|
|
207
|
+
isDialogOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "nucg-ad-overlay", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "nucg-ad-dialog", role: "alertdialog", "aria-modal": "true", children: [
|
|
208
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
209
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "nucg-ad-title", children: "Unsaved Changes" }),
|
|
210
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "nucg-ad-description", children: "You have unsaved changes. Do you want to save them before leaving?" })
|
|
211
|
+
] }),
|
|
212
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "nucg-ad-footer", children: [
|
|
213
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
214
|
+
"button",
|
|
215
|
+
{
|
|
216
|
+
className: "nucg-ad-btn nucg-ad-cancel",
|
|
217
|
+
onClick: handleStay,
|
|
218
|
+
children: "Stay"
|
|
219
|
+
}
|
|
220
|
+
),
|
|
221
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
222
|
+
"button",
|
|
223
|
+
{
|
|
224
|
+
className: "nucg-ad-btn nucg-ad-danger",
|
|
225
|
+
onClick: handleDiscard,
|
|
226
|
+
children: "Discard"
|
|
227
|
+
}
|
|
228
|
+
),
|
|
229
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
230
|
+
"button",
|
|
231
|
+
{
|
|
232
|
+
className: "nucg-ad-btn nucg-ad-primary",
|
|
233
|
+
onClick: handleSave,
|
|
234
|
+
children: "Save"
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
] })
|
|
238
|
+
] }) })
|
|
239
|
+
] });
|
|
240
|
+
};
|
|
241
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
242
|
+
0 && (module.exports = {
|
|
243
|
+
NextUnsavedChangesGuard
|
|
244
|
+
});
|
|
245
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.tsx","../src/NextUnsavedChangesGuard.tsx"],"sourcesContent":["export { NextUnsavedChangesGuard } from \"./NextUnsavedChangesGuard\";\n","\"use client\";\n\nimport { useState, useEffect, useRef } from \"react\";\nimport { useRouter } from \"next/navigation\";\n\nconst alertDialogCSS = `\n .nucg-ad-overlay {\n position: fixed;\n inset: 0;\n background: rgba(0,0,0,0.45);\n backdrop-filter: blur(2px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 50;\n animation: ad-fade-in .15s ease-out;\n }\n\n .nucg-ad-dialog {\n width: 100%;\n max-width: 420px;\n background: #ffffff;\n border-radius: 12px;\n padding: 24px;\n box-shadow:\n 0 10px 30px rgba(0,0,0,0.15),\n 0 1px 2px rgba(0,0,0,0.1);\n animation: ad-scale-in .15s ease-out;\n }\n\n .nucg-ad-title {\n font-size: 1.125rem;\n font-weight: 600;\n color: #111827;\n margin-bottom: 6px;\n }\n\n .nucg-ad-description {\n font-size: 0.95rem;\n color: #6b7280;\n line-height: 1.45;\n }\n\n .nucg-ad-footer {\n display: flex;\n justify-content: flex-end;\n gap: 10px;\n margin-top: 20px;\n }\n\n .nucg-ad-btn {\n padding: 8px 14px;\n border-radius: 8px;\n font-size: 0.875rem;\n border: none;\n cursor: pointer;\n transition: background .15s ease, transform .05s ease;\n }\n\n .nucg-ad-btn:active {\n transform: scale(0.97);\n }\n\n .nucg-ad-cancel {\n background: #f3f4f6;\n color: #111827;\n }\n .nucg-ad-cancel:hover {\n background: #e5e7eb;\n }\n\n .nucg-ad-danger {\n background: #fee2e2;\n color: #b91c1c;\n }\n .nucg-ad-danger:hover {\n background: #fecaca;\n }\n\n .nucg-ad-primary {\n background: #111827;\n color: #ffffff;\n }\n .nucg-ad-primary:hover {\n background: #1f2937;\n }\n\n @keyframes nucg-ad-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n }\n\n @keyframes nucg-ad-scale-in {\n from { transform: scale(.96); opacity: 0; }\n to { transform: scale(1); opacity: 1; }\n }\n`;\n\ninterface UnsavedChangesGuardProps {\n children: React.ReactNode;\n formDirty: boolean;\n saveData: () => Promise<void>;\n formErrors?: Record<string, any>;\n}\n\nexport const NextUnsavedChangesGuard = ({\n children,\n formDirty,\n saveData,\n formErrors = {},\n}: UnsavedChangesGuardProps) => {\n const [isDirty, setDirty] = useState(formDirty);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [nextRoute, setNextRoute] = useState<string | null>(null);\n const router = useRouter();\n const isRouteChanging = useRef(false);\n\n // Sync isDirty with formDirty prop\n useEffect(() => {\n setDirty(formDirty);\n }, [formDirty]);\n\n // Handle browser close/refresh\n useEffect(() => {\n const handleWindowClose = (e: BeforeUnloadEvent) => {\n if (!isDirty) return;\n e.preventDefault();\n e.returnValue =\n \"You have unsaved changes. Are you sure you want to leave?\";\n };\n\n window.addEventListener(\"beforeunload\", handleWindowClose);\n return () => {\n window.removeEventListener(\"beforeunload\", handleWindowClose);\n };\n }, [isDirty]);\n\n // Intercept route changes\n useEffect(() => {\n const originalPush = router.push;\n\n router.push = (url: string) => {\n if (isDirty && !isRouteChanging.current) {\n setNextRoute(url);\n setIsDialogOpen(true);\n return Promise.resolve(); // Prevent navigation without throwing\n }\n isRouteChanging.current = false;\n return originalPush(url);\n };\n\n return () => {\n router.push = originalPush;\n };\n }, [isDirty, router]);\n\n // Handle \"Save\" action\n const handleSave = async () => {\n if (Object.keys(formErrors).length === 0) {\n try {\n await saveData();\n setDirty(false);\n setIsDialogOpen(false);\n if (nextRoute) {\n isRouteChanging.current = true;\n router.push(nextRoute);\n }\n } catch (error) {\n console.log(\"Error saving changes\");\n }\n } else {\n setIsDialogOpen(false);\n console.log(\"Form contains errors\");\n }\n };\n\n // Handle \"Discard\" action\n const handleDiscard = () => {\n setDirty(false);\n setIsDialogOpen(false);\n if (nextRoute) {\n isRouteChanging.current = true;\n router.push(nextRoute);\n }\n };\n\n // Handle \"Stay\" action\n const handleStay = () => {\n setIsDialogOpen(false);\n setNextRoute(null);\n };\n\n function injectAlertDialogStyles() {\n if (typeof document === \"undefined\") return;\n\n if (!document.getElementById(\"alert-dialog-styles\")) {\n const style = document.createElement(\"style\");\n style.id = \"alert-dialog-styles\";\n style.textContent = alertDialogCSS;\n document.head.appendChild(style);\n }\n }\n\n useEffect(() => {\n injectAlertDialogStyles();\n }, []);\n\n return (\n <>\n {children}\n {isDialogOpen && (\n <div className=\"nucg-ad-overlay\">\n <div className=\"nucg-ad-dialog\" role=\"alertdialog\" aria-modal=\"true\">\n <div>\n <h2 className=\"nucg-ad-title\">Unsaved Changes</h2>\n <p className=\"nucg-ad-description\">\n You have unsaved changes. Do you want to save them before\n leaving?\n </p>\n </div>\n\n <div className=\"nucg-ad-footer\">\n <button\n className=\"nucg-ad-btn nucg-ad-cancel\"\n onClick={handleStay}\n >\n Stay\n </button>\n\n <button\n className=\"nucg-ad-btn nucg-ad-danger\"\n onClick={handleDiscard}\n >\n Discard\n </button>\n\n <button\n className=\"nucg-ad-btn nucg-ad-primary\"\n onClick={handleSave}\n >\n Save\n </button>\n </div>\n </div>\n </div>\n )}\n </>\n );\n};\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface UnsavedChangesGuardProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
formDirty: boolean;
|
|
6
|
+
saveData: () => Promise<void>;
|
|
7
|
+
formErrors?: Record<string, any>;
|
|
8
|
+
}
|
|
9
|
+
declare const NextUnsavedChangesGuard: ({ children, formDirty, saveData, formErrors, }: UnsavedChangesGuardProps) => react_jsx_runtime.JSX.Element;
|
|
10
|
+
|
|
11
|
+
export { NextUnsavedChangesGuard };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface UnsavedChangesGuardProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
formDirty: boolean;
|
|
6
|
+
saveData: () => Promise<void>;
|
|
7
|
+
formErrors?: Record<string, any>;
|
|
8
|
+
}
|
|
9
|
+
declare const NextUnsavedChangesGuard: ({ children, formDirty, saveData, formErrors, }: UnsavedChangesGuardProps) => react_jsx_runtime.JSX.Element;
|
|
10
|
+
|
|
11
|
+
export { NextUnsavedChangesGuard };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// src/NextUnsavedChangesGuard.tsx
|
|
2
|
+
import { useState, useEffect, useRef } from "react";
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
var alertDialogCSS = `
|
|
6
|
+
.nucg-ad-overlay {
|
|
7
|
+
position: fixed;
|
|
8
|
+
inset: 0;
|
|
9
|
+
background: rgba(0,0,0,0.45);
|
|
10
|
+
backdrop-filter: blur(2px);
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
z-index: 50;
|
|
15
|
+
animation: ad-fade-in .15s ease-out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.nucg-ad-dialog {
|
|
19
|
+
width: 100%;
|
|
20
|
+
max-width: 420px;
|
|
21
|
+
background: #ffffff;
|
|
22
|
+
border-radius: 12px;
|
|
23
|
+
padding: 24px;
|
|
24
|
+
box-shadow:
|
|
25
|
+
0 10px 30px rgba(0,0,0,0.15),
|
|
26
|
+
0 1px 2px rgba(0,0,0,0.1);
|
|
27
|
+
animation: ad-scale-in .15s ease-out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.nucg-ad-title {
|
|
31
|
+
font-size: 1.125rem;
|
|
32
|
+
font-weight: 600;
|
|
33
|
+
color: #111827;
|
|
34
|
+
margin-bottom: 6px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.nucg-ad-description {
|
|
38
|
+
font-size: 0.95rem;
|
|
39
|
+
color: #6b7280;
|
|
40
|
+
line-height: 1.45;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.nucg-ad-footer {
|
|
44
|
+
display: flex;
|
|
45
|
+
justify-content: flex-end;
|
|
46
|
+
gap: 10px;
|
|
47
|
+
margin-top: 20px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.nucg-ad-btn {
|
|
51
|
+
padding: 8px 14px;
|
|
52
|
+
border-radius: 8px;
|
|
53
|
+
font-size: 0.875rem;
|
|
54
|
+
border: none;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
transition: background .15s ease, transform .05s ease;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.nucg-ad-btn:active {
|
|
60
|
+
transform: scale(0.97);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.nucg-ad-cancel {
|
|
64
|
+
background: #f3f4f6;
|
|
65
|
+
color: #111827;
|
|
66
|
+
}
|
|
67
|
+
.nucg-ad-cancel:hover {
|
|
68
|
+
background: #e5e7eb;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.nucg-ad-danger {
|
|
72
|
+
background: #fee2e2;
|
|
73
|
+
color: #b91c1c;
|
|
74
|
+
}
|
|
75
|
+
.nucg-ad-danger:hover {
|
|
76
|
+
background: #fecaca;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.nucg-ad-primary {
|
|
80
|
+
background: #111827;
|
|
81
|
+
color: #ffffff;
|
|
82
|
+
}
|
|
83
|
+
.nucg-ad-primary:hover {
|
|
84
|
+
background: #1f2937;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@keyframes nucg-ad-fade-in {
|
|
88
|
+
from { opacity: 0; }
|
|
89
|
+
to { opacity: 1; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@keyframes nucg-ad-scale-in {
|
|
93
|
+
from { transform: scale(.96); opacity: 0; }
|
|
94
|
+
to { transform: scale(1); opacity: 1; }
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
var NextUnsavedChangesGuard = ({
|
|
98
|
+
children,
|
|
99
|
+
formDirty,
|
|
100
|
+
saveData,
|
|
101
|
+
formErrors = {}
|
|
102
|
+
}) => {
|
|
103
|
+
const [isDirty, setDirty] = useState(formDirty);
|
|
104
|
+
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
105
|
+
const [nextRoute, setNextRoute] = useState(null);
|
|
106
|
+
const router = useRouter();
|
|
107
|
+
const isRouteChanging = useRef(false);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
setDirty(formDirty);
|
|
110
|
+
}, [formDirty]);
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const handleWindowClose = (e) => {
|
|
113
|
+
if (!isDirty) return;
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
e.returnValue = "You have unsaved changes. Are you sure you want to leave?";
|
|
116
|
+
};
|
|
117
|
+
window.addEventListener("beforeunload", handleWindowClose);
|
|
118
|
+
return () => {
|
|
119
|
+
window.removeEventListener("beforeunload", handleWindowClose);
|
|
120
|
+
};
|
|
121
|
+
}, [isDirty]);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
const originalPush = router.push;
|
|
124
|
+
router.push = (url) => {
|
|
125
|
+
if (isDirty && !isRouteChanging.current) {
|
|
126
|
+
setNextRoute(url);
|
|
127
|
+
setIsDialogOpen(true);
|
|
128
|
+
return Promise.resolve();
|
|
129
|
+
}
|
|
130
|
+
isRouteChanging.current = false;
|
|
131
|
+
return originalPush(url);
|
|
132
|
+
};
|
|
133
|
+
return () => {
|
|
134
|
+
router.push = originalPush;
|
|
135
|
+
};
|
|
136
|
+
}, [isDirty, router]);
|
|
137
|
+
const handleSave = async () => {
|
|
138
|
+
if (Object.keys(formErrors).length === 0) {
|
|
139
|
+
try {
|
|
140
|
+
await saveData();
|
|
141
|
+
setDirty(false);
|
|
142
|
+
setIsDialogOpen(false);
|
|
143
|
+
if (nextRoute) {
|
|
144
|
+
isRouteChanging.current = true;
|
|
145
|
+
router.push(nextRoute);
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.log("Error saving changes");
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
setIsDialogOpen(false);
|
|
152
|
+
console.log("Form contains errors");
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const handleDiscard = () => {
|
|
156
|
+
setDirty(false);
|
|
157
|
+
setIsDialogOpen(false);
|
|
158
|
+
if (nextRoute) {
|
|
159
|
+
isRouteChanging.current = true;
|
|
160
|
+
router.push(nextRoute);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const handleStay = () => {
|
|
164
|
+
setIsDialogOpen(false);
|
|
165
|
+
setNextRoute(null);
|
|
166
|
+
};
|
|
167
|
+
function injectAlertDialogStyles() {
|
|
168
|
+
if (typeof document === "undefined") return;
|
|
169
|
+
if (!document.getElementById("alert-dialog-styles")) {
|
|
170
|
+
const style = document.createElement("style");
|
|
171
|
+
style.id = "alert-dialog-styles";
|
|
172
|
+
style.textContent = alertDialogCSS;
|
|
173
|
+
document.head.appendChild(style);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
injectAlertDialogStyles();
|
|
178
|
+
}, []);
|
|
179
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
180
|
+
children,
|
|
181
|
+
isDialogOpen && /* @__PURE__ */ jsx("div", { className: "nucg-ad-overlay", children: /* @__PURE__ */ jsxs("div", { className: "nucg-ad-dialog", role: "alertdialog", "aria-modal": "true", children: [
|
|
182
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
183
|
+
/* @__PURE__ */ jsx("h2", { className: "nucg-ad-title", children: "Unsaved Changes" }),
|
|
184
|
+
/* @__PURE__ */ jsx("p", { className: "nucg-ad-description", children: "You have unsaved changes. Do you want to save them before leaving?" })
|
|
185
|
+
] }),
|
|
186
|
+
/* @__PURE__ */ jsxs("div", { className: "nucg-ad-footer", children: [
|
|
187
|
+
/* @__PURE__ */ jsx(
|
|
188
|
+
"button",
|
|
189
|
+
{
|
|
190
|
+
className: "nucg-ad-btn nucg-ad-cancel",
|
|
191
|
+
onClick: handleStay,
|
|
192
|
+
children: "Stay"
|
|
193
|
+
}
|
|
194
|
+
),
|
|
195
|
+
/* @__PURE__ */ jsx(
|
|
196
|
+
"button",
|
|
197
|
+
{
|
|
198
|
+
className: "nucg-ad-btn nucg-ad-danger",
|
|
199
|
+
onClick: handleDiscard,
|
|
200
|
+
children: "Discard"
|
|
201
|
+
}
|
|
202
|
+
),
|
|
203
|
+
/* @__PURE__ */ jsx(
|
|
204
|
+
"button",
|
|
205
|
+
{
|
|
206
|
+
className: "nucg-ad-btn nucg-ad-primary",
|
|
207
|
+
onClick: handleSave,
|
|
208
|
+
children: "Save"
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
] })
|
|
212
|
+
] }) })
|
|
213
|
+
] });
|
|
214
|
+
};
|
|
215
|
+
export {
|
|
216
|
+
NextUnsavedChangesGuard
|
|
217
|
+
};
|
|
218
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/NextUnsavedChangesGuard.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState, useEffect, useRef } from \"react\";\nimport { useRouter } from \"next/navigation\";\n\nconst alertDialogCSS = `\n .nucg-ad-overlay {\n position: fixed;\n inset: 0;\n background: rgba(0,0,0,0.45);\n backdrop-filter: blur(2px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 50;\n animation: ad-fade-in .15s ease-out;\n }\n\n .nucg-ad-dialog {\n width: 100%;\n max-width: 420px;\n background: #ffffff;\n border-radius: 12px;\n padding: 24px;\n box-shadow:\n 0 10px 30px rgba(0,0,0,0.15),\n 0 1px 2px rgba(0,0,0,0.1);\n animation: ad-scale-in .15s ease-out;\n }\n\n .nucg-ad-title {\n font-size: 1.125rem;\n font-weight: 600;\n color: #111827;\n margin-bottom: 6px;\n }\n\n .nucg-ad-description {\n font-size: 0.95rem;\n color: #6b7280;\n line-height: 1.45;\n }\n\n .nucg-ad-footer {\n display: flex;\n justify-content: flex-end;\n gap: 10px;\n margin-top: 20px;\n }\n\n .nucg-ad-btn {\n padding: 8px 14px;\n border-radius: 8px;\n font-size: 0.875rem;\n border: none;\n cursor: pointer;\n transition: background .15s ease, transform .05s ease;\n }\n\n .nucg-ad-btn:active {\n transform: scale(0.97);\n }\n\n .nucg-ad-cancel {\n background: #f3f4f6;\n color: #111827;\n }\n .nucg-ad-cancel:hover {\n background: #e5e7eb;\n }\n\n .nucg-ad-danger {\n background: #fee2e2;\n color: #b91c1c;\n }\n .nucg-ad-danger:hover {\n background: #fecaca;\n }\n\n .nucg-ad-primary {\n background: #111827;\n color: #ffffff;\n }\n .nucg-ad-primary:hover {\n background: #1f2937;\n }\n\n @keyframes nucg-ad-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n }\n\n @keyframes nucg-ad-scale-in {\n from { transform: scale(.96); opacity: 0; }\n to { transform: scale(1); opacity: 1; }\n }\n`;\n\ninterface UnsavedChangesGuardProps {\n children: React.ReactNode;\n formDirty: boolean;\n saveData: () => Promise<void>;\n formErrors?: Record<string, any>;\n}\n\nexport const NextUnsavedChangesGuard = ({\n children,\n formDirty,\n saveData,\n formErrors = {},\n}: UnsavedChangesGuardProps) => {\n const [isDirty, setDirty] = useState(formDirty);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [nextRoute, setNextRoute] = useState<string | null>(null);\n const router = useRouter();\n const isRouteChanging = useRef(false);\n\n // Sync isDirty with formDirty prop\n useEffect(() => {\n setDirty(formDirty);\n }, [formDirty]);\n\n // Handle browser close/refresh\n useEffect(() => {\n const handleWindowClose = (e: BeforeUnloadEvent) => {\n if (!isDirty) return;\n e.preventDefault();\n e.returnValue =\n \"You have unsaved changes. Are you sure you want to leave?\";\n };\n\n window.addEventListener(\"beforeunload\", handleWindowClose);\n return () => {\n window.removeEventListener(\"beforeunload\", handleWindowClose);\n };\n }, [isDirty]);\n\n // Intercept route changes\n useEffect(() => {\n const originalPush = router.push;\n\n router.push = (url: string) => {\n if (isDirty && !isRouteChanging.current) {\n setNextRoute(url);\n setIsDialogOpen(true);\n return Promise.resolve(); // Prevent navigation without throwing\n }\n isRouteChanging.current = false;\n return originalPush(url);\n };\n\n return () => {\n router.push = originalPush;\n };\n }, [isDirty, router]);\n\n // Handle \"Save\" action\n const handleSave = async () => {\n if (Object.keys(formErrors).length === 0) {\n try {\n await saveData();\n setDirty(false);\n setIsDialogOpen(false);\n if (nextRoute) {\n isRouteChanging.current = true;\n router.push(nextRoute);\n }\n } catch (error) {\n console.log(\"Error saving changes\");\n }\n } else {\n setIsDialogOpen(false);\n console.log(\"Form contains errors\");\n }\n };\n\n // Handle \"Discard\" action\n const handleDiscard = () => {\n setDirty(false);\n setIsDialogOpen(false);\n if (nextRoute) {\n isRouteChanging.current = true;\n router.push(nextRoute);\n }\n };\n\n // Handle \"Stay\" action\n const handleStay = () => {\n setIsDialogOpen(false);\n setNextRoute(null);\n };\n\n function injectAlertDialogStyles() {\n if (typeof document === \"undefined\") return;\n\n if (!document.getElementById(\"alert-dialog-styles\")) {\n const style = document.createElement(\"style\");\n style.id = \"alert-dialog-styles\";\n style.textContent = alertDialogCSS;\n document.head.appendChild(style);\n }\n }\n\n useEffect(() => {\n injectAlertDialogStyles();\n }, []);\n\n return (\n <>\n {children}\n {isDialogOpen && (\n <div className=\"nucg-ad-overlay\">\n <div className=\"nucg-ad-dialog\" role=\"alertdialog\" aria-modal=\"true\">\n <div>\n <h2 className=\"nucg-ad-title\">Unsaved Changes</h2>\n <p className=\"nucg-ad-description\">\n You have unsaved changes. Do you want to save them before\n leaving?\n </p>\n </div>\n\n <div className=\"nucg-ad-footer\">\n <button\n className=\"nucg-ad-btn nucg-ad-cancel\"\n onClick={handleStay}\n >\n Stay\n </button>\n\n <button\n className=\"nucg-ad-btn nucg-ad-danger\"\n onClick={handleDiscard}\n >\n Discard\n </button>\n\n <button\n className=\"nucg-ad-btn nucg-ad-primary\"\n onClick={handleSave}\n >\n Save\n </button>\n </div>\n </div>\n </div>\n )}\n </>\n );\n};\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "next-unsaved-changes-guard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight, customizable React component for Next.js 13+ (App Router) that prevents users from accidentally leaving a page with unsaved changes.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/shibu2000/npm-packages.git",
|
|
10
|
+
"directory": "next-unsaved-changes-guard"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/shibu2000/npm-packages/tree/main/next-unsaved-changes-guard#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/shibu2000/npm-packages/issues"
|
|
15
|
+
},
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "Shibu Dhara",
|
|
18
|
+
"email": "shibudhara147@gmail.com",
|
|
19
|
+
"url": "https://github.com/shibu2000"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"require": "./dist/index.cjs"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"prepublishOnly": "npm run build"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"next": ">=13",
|
|
38
|
+
"react": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/react": "^19",
|
|
42
|
+
"next": "^16.1.1",
|
|
43
|
+
"tsup": "^8.5.1",
|
|
44
|
+
"typescript": "^5.9.3"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"nextjs",
|
|
48
|
+
"react",
|
|
49
|
+
"unsaved-changes",
|
|
50
|
+
"form-guard",
|
|
51
|
+
"route-guard",
|
|
52
|
+
"navigation-guard",
|
|
53
|
+
"prevent-navigation",
|
|
54
|
+
"beforeunload",
|
|
55
|
+
"nextjs-app-router",
|
|
56
|
+
"next-navigation",
|
|
57
|
+
"app-router",
|
|
58
|
+
"form-dirty",
|
|
59
|
+
"data-loss-prevention",
|
|
60
|
+
"modal",
|
|
61
|
+
"dialog",
|
|
62
|
+
"typescript"
|
|
63
|
+
]
|
|
64
|
+
}
|