nexus-shared 1.1.17 → 1.1.19
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/package.json +1 -1
- package/src/api-services/system-service.tsx +1 -2
- package/src/client.ts +3 -2
- package/src/components/documents/button.tsx +5 -1
- package/src/components/inputs/input-box.tsx +4 -2
- package/src/components/inputs/input-form.tsx +126 -14
- package/src/components/layouts/global-layout.tsx +4 -2
- package/src/components/layouts/layout-helpers.tsx +23 -23
- package/src/helpers/browser-helpers.tsx +41 -1
- package/src/helpers/input-helpers.tsx +34 -4
- package/src/helpers/utility-helpers.tsx +1 -0
- package/src/helpers/validation-helpers.tsx +302 -0
- package/src/index.ts +3 -18
- package/src/interface.ts +4 -1
- package/src/interfaces/button-interfaces.tsx +2 -2
- package/src/interfaces/exception-interfaces.tsx +99 -0
- package/src/interfaces/http-interfaces.tsx +120 -3
- package/src/interfaces/input-interfaces.tsx +61 -11
- package/src/interfaces/message-interfaces.tsx +32 -0
- package/src/interfaces/permission-interfaces.tsx +4 -4
- package/src/interfaces/storage-interfaces.tsx +1 -0
- package/src/interfaces/system-interfaces.tsx +2 -2
- package/src/interfaces/type-interfaces.tsx +8 -2
- package/src/interfaces/user-interfaces.tsx +17 -0
- package/src/nexus.environments.tsx +27 -17
- package/src/proxy-api/proxy-backend.tsx +60 -0
- package/src/proxy-api/proxy-constants.tsx +28 -0
- package/src/proxy-api/proxy-helpers.tsx +91 -0
- package/src/proxy-api-client.tsx +1 -0
- package/src/proxy-api-server.tsx +2 -0
- package/src/services/http-client-services.tsx +75 -0
- package/src/services/http-services.tsx +158 -0
- package/src/services/loader-service.tsx +5 -4
- package/src/services/localstorage-service.tsx +59 -0
- package/src/services/message-services.tsx +383 -0
- package/src/sso-config/auth-token-validation.ts +20 -0
- package/src/sso-config/callback-route.tsx +34 -0
- package/src/sso-config/cookie-encryption.ts +51 -0
- package/src/sso-config/cookie-helpers.tsx +77 -0
- package/src/sso-config/forgot-password-route.tsx +36 -0
- package/src/sso-config/oauth-callback-state.ts +58 -0
- package/src/sso-config/pkce-helpers.ts +16 -0
- package/src/sso-config/provider-complete-route.tsx +15 -0
- package/src/sso-config/redirect-context-actions.tsx +46 -0
- package/src/sso-config/redirect-context-helpers.tsx +144 -0
- package/src/sso-config/redirect-context-persist.tsx +18 -0
- package/src/sso-config/redirect-context-route.tsx +45 -0
- package/src/sso-config/refresh-route.tsx +22 -0
- package/src/sso-config/sign-in-route.tsx +69 -0
- package/src/sso-config/sign-out-route.tsx +53 -0
- package/src/sso-config/sign-up-route.tsx +46 -0
- package/src/sso-config/sso-backend-flow.ts +45 -0
- package/src/sso-config/sso-complete-route.tsx +46 -0
- package/src/sso-config/sso-config.ts +57 -0
- package/src/sso-config/sso-interfaces.tsx +68 -0
- package/src/sso-config/sso-response-helpers.tsx +20 -0
- package/src/sso-server.tsx +21 -1
- package/src/styles/nexus.dialog.css +3 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side HTTP utilities for Route Handlers, Server Components, and Server Actions.
|
|
3
|
+
* For browser/client components use `http-client-services` (`Post`, `Get`, …) via `nexus-shared/client`.
|
|
4
|
+
*/
|
|
5
|
+
import { Delay } from "../helpers/utility-helpers";
|
|
6
|
+
import { EActions, EBody, EModules, GetMethod, IErrorRespose, ISuccessResponse, UN } from "../interface";
|
|
7
|
+
import { EErrorCodes, EExceptionCategories } from "../interfaces/exception-interfaces";
|
|
8
|
+
import { APIS, NEXUS_CONFIG, NEXUS_INFO } from "../nexus.environments";
|
|
9
|
+
|
|
10
|
+
const MAX_RETRIES = NEXUS_INFO.IS_PRODUCTION ? 2 : 1;
|
|
11
|
+
const RETRY_DELAY_MS = NEXUS_INFO.IS_PRODUCTION ? 50 : 1;
|
|
12
|
+
|
|
13
|
+
export const BuildApiHeader = (formdata?: any, httpMethod?: EActions | string, bodyType: EBody | UN = EBody.JSON, token?: string | UN, revalidate?: number | UN, browserId?: string | UN): RequestInit & { next?: { revalidate: number } } => {
|
|
14
|
+
httpMethod = httpMethod ?? "GET";
|
|
15
|
+
bodyType = bodyType ?? EBody.JSON;
|
|
16
|
+
|
|
17
|
+
const headers = new Headers();
|
|
18
|
+
if (token && token.length > 50) headers.append("authorization", `${token}`);
|
|
19
|
+
if (browserId) headers.append("bid", browserId);
|
|
20
|
+
headers.append("Content-Type", bodyType);
|
|
21
|
+
|
|
22
|
+
const options: RequestInit & { next?: { revalidate: number } } = {
|
|
23
|
+
method: typeof httpMethod === "string" ? httpMethod : GetMethod(httpMethod),
|
|
24
|
+
headers,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (formdata) options.body = bodyType === EBody.JSON ? JSON.stringify(formdata) : formdata;
|
|
28
|
+
|
|
29
|
+
if (!revalidate || revalidate <= 0) {
|
|
30
|
+
// No caching
|
|
31
|
+
options.cache = "no-store";
|
|
32
|
+
} else {
|
|
33
|
+
// Use Next.js ISR caching
|
|
34
|
+
options.next = { revalidate: revalidate ?? 0 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return options;
|
|
38
|
+
};
|
|
39
|
+
const shouldRetry = (error: any) => error.name === "AbortError" || error.code === "ECONNREFUSED" || error.message.includes("fetch") || error.message.includes("network");
|
|
40
|
+
const safeFetch = async (fetchFunc: () => Promise<Response>): Promise<ISuccessResponse | IErrorRespose> => {
|
|
41
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetchFunc();
|
|
44
|
+
if (!response.ok) return await handleResponse(response);
|
|
45
|
+
return await response.json();
|
|
46
|
+
} catch (error: any) {
|
|
47
|
+
if (attempt < MAX_RETRIES - 1 && shouldRetry(error)) {
|
|
48
|
+
await Delay(RETRY_DELAY_MS * (attempt + 1));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
return handleNetworkError(error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return handleError({ status: 500, message: "Maximum retry attempts exceeded." });
|
|
55
|
+
};
|
|
56
|
+
const fetchWithTimeout = async (url: string, options?: RequestInit, timeout = NEXUS_CONFIG.DEFAULT_HTTP_REQUEST_TIMEOUT): Promise<Response> => {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(url, { ...options, signal: controller.signal });
|
|
61
|
+
return response;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw error;
|
|
64
|
+
} finally {
|
|
65
|
+
clearTimeout(id);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
export const HttpGet = async (apiUrl: string, token?: string | UN, revalidate?: number | UN, browserId?: string | UN) => {
|
|
69
|
+
return await safeFetch(() => fetchWithTimeout(apiUrl, BuildApiHeader(undefined, EActions.Get, EBody.JSON, token, revalidate, browserId)));
|
|
70
|
+
};
|
|
71
|
+
export const HttpPost = async (apiUrl: string, body?: any | UN, bodyType?: EBody | UN, token?: string | UN, revalidate?: number | UN, browserId?: string | UN) => {
|
|
72
|
+
return await safeFetch(() => fetchWithTimeout(apiUrl, BuildApiHeader(body, EActions.Post, bodyType, token, revalidate, browserId)));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const HttpPut = async (apiUrl: string, body?: any | UN, bodyType?: EBody | UN, token?: string | UN, revalidate?: number | UN, browserId?: string | UN) => {
|
|
76
|
+
return await safeFetch(() => fetchWithTimeout(apiUrl, BuildApiHeader(body, EActions.Put, bodyType, token, revalidate, browserId)));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const HttpDelete = async (apiUrl: string, token?: string | UN, revalidate?: number | UN, browserId?: string | UN) => {
|
|
80
|
+
return await safeFetch(() => fetchWithTimeout(apiUrl, BuildApiHeader(undefined, EActions.Delete, undefined, token, revalidate, browserId)));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const HttpFetch = async (apiUrl: string, method: EActions | string, body?: any, bodyType?: EBody, token?: string | UN, revalidate?: number, browserId?: string | UN): Promise<Response | Error> => {
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(apiUrl, BuildApiHeader(body, method, bodyType, token, revalidate, browserId));
|
|
86
|
+
return response;
|
|
87
|
+
} catch (error: Error | any) {
|
|
88
|
+
return error;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const GetRootApi = (nexusModule: EModules) => {
|
|
93
|
+
switch (nexusModule) {
|
|
94
|
+
case EModules.None:
|
|
95
|
+
return "/api";
|
|
96
|
+
case EModules.Authentication:
|
|
97
|
+
return APIS.AUTHENTICATION;
|
|
98
|
+
case EModules.Configuration:
|
|
99
|
+
return APIS.CONFIGURATION;
|
|
100
|
+
case EModules.Enterprise:
|
|
101
|
+
return APIS.ENTERPRISE;
|
|
102
|
+
case EModules.Drive:
|
|
103
|
+
return APIS.DRIVE;
|
|
104
|
+
case EModules.Message:
|
|
105
|
+
return APIS.MESSAGE;
|
|
106
|
+
case EModules.Accounting:
|
|
107
|
+
return APIS.ACCOUNTING;
|
|
108
|
+
case EModules.Report:
|
|
109
|
+
return APIS.REPORT;
|
|
110
|
+
default:
|
|
111
|
+
throw new Error("Invalid Nexus Module");
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const handleResponse = async (response: Response): Promise<IErrorRespose> => {
|
|
115
|
+
const status = response.status;
|
|
116
|
+
try {
|
|
117
|
+
const errorData = await response.json();
|
|
118
|
+
return handleError({ ...errorData, status });
|
|
119
|
+
} catch {
|
|
120
|
+
let message = "Unexpected server error.";
|
|
121
|
+
switch (status) {
|
|
122
|
+
case 401:
|
|
123
|
+
case 403:
|
|
124
|
+
message = "You do not have permission to access this resource.";
|
|
125
|
+
break;
|
|
126
|
+
case 404:
|
|
127
|
+
case 410:
|
|
128
|
+
message = "Resource not found.";
|
|
129
|
+
break;
|
|
130
|
+
case 405:
|
|
131
|
+
message = "Method not allowed.";
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
return handleError({ status, message });
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
export const handleError = (error: any): IErrorRespose => {
|
|
138
|
+
return {
|
|
139
|
+
isSuccess: false,
|
|
140
|
+
code: error?.code,
|
|
141
|
+
message: error?.message || "An unknown error occurred.",
|
|
142
|
+
messageCode: error?.messageCode || "GENERIC_ERROR",
|
|
143
|
+
exceptionCategory: error?.exceptionCategory || EExceptionCategories.Application,
|
|
144
|
+
errorCode: error?.errorCode || EErrorCodes.Unknown,
|
|
145
|
+
inputName: error?.inputName || undefined,
|
|
146
|
+
exception: error?.exception || undefined,
|
|
147
|
+
result: error?.result || undefined,
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
const handleNetworkError = (error: any): IErrorRespose => {
|
|
151
|
+
console.error("\n\nNetwork error encountered:", error);
|
|
152
|
+
return handleError({
|
|
153
|
+
status: 503,
|
|
154
|
+
message: "Unable to connect to the server. Please check your network or backend status.",
|
|
155
|
+
key: "NETWORK_ERROR",
|
|
156
|
+
exception: error,
|
|
157
|
+
});
|
|
158
|
+
};
|
|
@@ -74,7 +74,7 @@ const _updateLoaderAttributes = (loaderElement: HTMLElement, type: EAnimationCla
|
|
|
74
74
|
loaderElement.setAttribute(LOADER_DATA_SIZE_ATTRIBUTE, size);
|
|
75
75
|
loaderElement.setAttribute(LOADER_DATA_DISABLED_INPUTS_ATTRIBUTE, disabledInputs ? "true" : "false");
|
|
76
76
|
};
|
|
77
|
-
const _createLoaderElement = (parent: HTMLElement, type: EAnimationClasses, size: ESizes, showLogo: boolean, disabledInputs: boolean, background: EBackgrounds): HTMLElement | null => {
|
|
77
|
+
const _createLoaderElement = (parent: HTMLElement, type: EAnimationClasses, size: ESizes, showLogo: boolean, disabledInputs: boolean, background: EBackgrounds, defaultClass?: string | UN): HTMLElement | null => {
|
|
78
78
|
const loaderElement = _findExistingLoader(parent);
|
|
79
79
|
if (loaderElement && !loaderElement.classList.contains(DISPOSED_LOADER_CLASS)) {
|
|
80
80
|
const count = parseInt(loaderElement.getAttribute(LOADER_DATA_COUNT_ATTRIBUTE) ?? "0") + 1;
|
|
@@ -86,6 +86,7 @@ const _createLoaderElement = (parent: HTMLElement, type: EAnimationClasses, size
|
|
|
86
86
|
const newLoaderElement = document.createElement("div");
|
|
87
87
|
newLoaderElement.id = LOADER_IDENTITY;
|
|
88
88
|
newLoaderElement.classList.add("nexus-container", "o0", EAnimationClasses.FADE_IN, background);
|
|
89
|
+
if (defaultClass) newLoaderElement.classList.add(defaultClass);
|
|
89
90
|
_updateLoaderAttributes(newLoaderElement, type, size, disabledInputs, 1);
|
|
90
91
|
|
|
91
92
|
const loader = document.createElement("div");
|
|
@@ -97,7 +98,7 @@ const _createLoaderElement = (parent: HTMLElement, type: EAnimationClasses, size
|
|
|
97
98
|
loader.innerText = "Loading...";
|
|
98
99
|
loader.classList.add("tx52");
|
|
99
100
|
loader.style.fontSize = "1.5rem";
|
|
100
|
-
} else if (showLogo) {
|
|
101
|
+
} else if (showLogo && type !== EAnimationClasses.SLIDE_BAR && type !== EAnimationClasses.BOUNCE_BAR) {
|
|
101
102
|
_updateLoaderProperties(newLoaderElement, type, size);
|
|
102
103
|
|
|
103
104
|
const logoElement = document.createElement("img");
|
|
@@ -123,7 +124,7 @@ const _createLoaderElement = (parent: HTMLElement, type: EAnimationClasses, size
|
|
|
123
124
|
return newLoaderElement;
|
|
124
125
|
};
|
|
125
126
|
|
|
126
|
-
export const ShowLoader = async (parent?: String | HTMLElement | UN, type?: EAnimationClasses | UN, disabledInputs?: boolean | UN, size?: ESizes | UN, delay?: number | UN, showLogo?: boolean | UN, background?: EBackgrounds | UN): Promise<HTMLElement | null> => {
|
|
127
|
+
export const ShowLoader = async (parent?: String | HTMLElement | UN, type?: EAnimationClasses | UN, disabledInputs?: boolean | UN, size?: ESizes | UN, delay?: number | UN, showLogo?: boolean | UN, background?: EBackgrounds | UN, defaultClass?: string | UN): Promise<HTMLElement | null> => {
|
|
127
128
|
if (!IsBrowser()) return Promise.resolve(null);
|
|
128
129
|
type = type ?? DEFAULT_LOADER_TYPE;
|
|
129
130
|
size = size ?? DEFAULT_LOADER_SIZE;
|
|
@@ -133,7 +134,7 @@ export const ShowLoader = async (parent?: String | HTMLElement | UN, type?: EAni
|
|
|
133
134
|
const timeout = setTimeout(() => {
|
|
134
135
|
clearTimeout(timeout);
|
|
135
136
|
const parentElement = _findParentElement(parent);
|
|
136
|
-
const loaderElement = _createLoaderElement(parentElement, type, size, showLogo ?? true, disabledInputs ?? false, background);
|
|
137
|
+
const loaderElement = _createLoaderElement(parentElement, type, size, showLogo ?? true, disabledInputs ?? false, background, defaultClass);
|
|
137
138
|
resolve(loaderElement);
|
|
138
139
|
}, delay ?? DEFAULT_LOADER_DELAY);
|
|
139
140
|
});
|
|
@@ -1,30 +1,67 @@
|
|
|
1
1
|
import { CreateUpdatePreference } from "../api-services/preference-service";
|
|
2
2
|
import { IsBrowser } from "../helpers/browser-helpers";
|
|
3
3
|
import { Debounce } from "../helpers/utility-helpers";
|
|
4
|
+
import { EFlexPositions } from "../interface";
|
|
4
5
|
import { LOCAL_STORAGE_KEYS } from "../interfaces/storage-interfaces";
|
|
5
6
|
import { EPreferenceTargets } from "../interfaces/system-interfaces";
|
|
6
7
|
import { IThemeInformation } from "../interfaces/theme-interfaces";
|
|
7
8
|
import { O } from "../nexus-client";
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Create a debounce function to update the preference in the server
|
|
12
|
+
* This is used to update the preference in the server
|
|
13
|
+
* @param CreateUpdatePreference The function to create the update preference
|
|
14
|
+
* @param debounceTime The debounce time in milliseconds
|
|
15
|
+
* @returns The debounce function
|
|
16
|
+
*/
|
|
9
17
|
const _createUpdatePreferenceDebounce = Debounce(CreateUpdatePreference, 1000);
|
|
10
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Sync the value of the local storage to the server
|
|
21
|
+
* This is used to sync the value of the local storage to the server
|
|
22
|
+
* @param key The key of the local storage
|
|
23
|
+
* @param value The value to set
|
|
24
|
+
* @param isSyncWithServer Whether to sync with the server
|
|
25
|
+
* @param target The target of the local storage
|
|
26
|
+
*/
|
|
11
27
|
const _syncStorageToServer = async (key: string, value: string | null, isSyncWithServer?: boolean, target?: EPreferenceTargets) => {
|
|
12
28
|
if (!IsBrowser() || !isSyncWithServer || !target) return;
|
|
13
29
|
|
|
14
30
|
await _createUpdatePreferenceDebounce(target, key, value);
|
|
15
31
|
};
|
|
16
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Set the value of the local storage
|
|
35
|
+
* This is used to set the value of the local storage
|
|
36
|
+
* @param key The key of the local storage
|
|
37
|
+
* @param value The value to set
|
|
38
|
+
* @param isSyncWithServer Whether to sync with the server
|
|
39
|
+
* @param target The target of the local storage
|
|
40
|
+
*/
|
|
17
41
|
export const SetLocalStorage = (key: string, value: string, isSyncWithServer?: boolean, target?: EPreferenceTargets) => {
|
|
18
42
|
if (!IsBrowser()) return;
|
|
19
43
|
|
|
20
44
|
_syncStorageToServer(key, value, isSyncWithServer, target);
|
|
21
45
|
localStorage.setItem(key, value);
|
|
22
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Remove the value of the local storage
|
|
49
|
+
* This is used to remove the value of the local storage
|
|
50
|
+
* @param key The key of the local storage
|
|
51
|
+
* @param isSyncWithServer Whether to sync with the server
|
|
52
|
+
* @param target The target of the local storage
|
|
53
|
+
*/
|
|
23
54
|
export const RemoveLocalStorage = (key: string, isSyncWithServer?: boolean, target?: EPreferenceTargets) => {
|
|
24
55
|
_syncStorageToServer(key, null, isSyncWithServer, target);
|
|
25
56
|
localStorage.removeItem(key);
|
|
26
57
|
};
|
|
27
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Get the value of the local storage
|
|
61
|
+
* This is used to get the value of the local storage
|
|
62
|
+
* @param key The key of the local storage
|
|
63
|
+
* @returns The value of the local storage
|
|
64
|
+
*/
|
|
28
65
|
export function GetLocalStorage(key: string): string | null {
|
|
29
66
|
if (!IsBrowser()) return null;
|
|
30
67
|
const data = localStorage.getItem(key);
|
|
@@ -32,6 +69,11 @@ export function GetLocalStorage(key: string): string | null {
|
|
|
32
69
|
return data;
|
|
33
70
|
}
|
|
34
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Set the current theme in the local storage
|
|
74
|
+
* This is used to determine the current theme in the application
|
|
75
|
+
* @param themeCode The code of the theme to set
|
|
76
|
+
*/
|
|
35
77
|
export const SetCurrentTheme = (themeCode: string) => SetLocalStorage(LOCAL_STORAGE_KEYS.ACTIVE_THEME, themeCode, true, EPreferenceTargets.User);
|
|
36
78
|
export const GetCurrentTheme = (): IThemeInformation => {
|
|
37
79
|
const themeInformations = O.systemThemes;
|
|
@@ -41,3 +83,20 @@ export const GetCurrentTheme = (): IThemeInformation => {
|
|
|
41
83
|
return themeInformations.find((theme) => theme.systemName === activeTheme) || themeInformations[0];
|
|
42
84
|
};
|
|
43
85
|
export const GetCurrentThemeName = (): string => GetCurrentTheme().systemName;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the position of the form buttons from the local storage
|
|
89
|
+
* This is used to determine the position of the form buttons in the form
|
|
90
|
+
* @returns The position of the form buttons
|
|
91
|
+
*/
|
|
92
|
+
export const GetFormButtonPosition = (): EFlexPositions => {
|
|
93
|
+
const position = GetLocalStorage(LOCAL_STORAGE_KEYS.FORM_BUTTON_POSITION);
|
|
94
|
+
if (!position) return EFlexPositions.Left;
|
|
95
|
+
return position as EFlexPositions;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Set the position of the form buttons in the local storage
|
|
99
|
+
* This is used to determine the position of the form buttons in the form
|
|
100
|
+
* @param position The position of the form buttons
|
|
101
|
+
*/
|
|
102
|
+
export const SetFormButtonPosition = (position: EFlexPositions) => SetLocalStorage(LOCAL_STORAGE_KEYS.FORM_BUTTON_POSITION, position, true, EPreferenceTargets.User);
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { DEFAULT_ANIMATION_DURATION, DEFAULT_ICON_ELEMENT_NAME, EAnimationClasses, EBackgrounds, ECallbackTypes, EIcons, EIconTypes, EMessageBoxButtons, EMessageIcons, EMessageResult, EMessageTypes, EOpacity, ESizes, PopupMessageCallback, UN } from "nexus-shared";
|
|
2
|
+
import { CreateIconBox, IsBrowser, PxToRem, RemoveElement } from "nexus-shared/client";
|
|
3
|
+
|
|
4
|
+
const MESSAGE_IDENTITY = "nexus-message";
|
|
5
|
+
const MESSAGE_GAP_REM = 0.8;
|
|
6
|
+
const MESSAGE_TOP_REM = 1;
|
|
7
|
+
const PROGRESS_BAR_HEIGHT_REM = 0.2;
|
|
8
|
+
|
|
9
|
+
/** Newest first — index 0 is the top message. */
|
|
10
|
+
const activeMessages: HTMLElement[] = [];
|
|
11
|
+
const messageProgressCleanups = new WeakMap<HTMLElement, () => void>();
|
|
12
|
+
let reflowScheduled = false;
|
|
13
|
+
|
|
14
|
+
const stopMessageProgress = (messageElement: HTMLElement) => {
|
|
15
|
+
messageProgressCleanups.get(messageElement)?.();
|
|
16
|
+
messageProgressCleanups.delete(messageElement);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const startMessageProgress = (messageElement: HTMLElement, progressFillElement: HTMLElement, durationMilliseconds: number, onComplete: () => void) => {
|
|
20
|
+
let elapsedMilliseconds = 0;
|
|
21
|
+
let lastFrameTimestamp: number | null = null;
|
|
22
|
+
let animationFrameId = 0;
|
|
23
|
+
|
|
24
|
+
const cancelAnimation = () => {
|
|
25
|
+
if (animationFrameId) cancelAnimationFrame(animationFrameId);
|
|
26
|
+
animationFrameId = 0;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const finishProgress = () => {
|
|
30
|
+
cancelAnimation();
|
|
31
|
+
messageProgressCleanups.delete(messageElement);
|
|
32
|
+
onComplete();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const updateProgress = (frameTimestamp: number) => {
|
|
36
|
+
animationFrameId = 0;
|
|
37
|
+
if (lastFrameTimestamp !== null) {
|
|
38
|
+
elapsedMilliseconds += frameTimestamp - lastFrameTimestamp;
|
|
39
|
+
progressFillElement.style.width = `${Math.min((elapsedMilliseconds / durationMilliseconds) * 100, 100)}%`;
|
|
40
|
+
if (elapsedMilliseconds >= durationMilliseconds) return finishProgress();
|
|
41
|
+
}
|
|
42
|
+
lastFrameTimestamp = frameTimestamp;
|
|
43
|
+
animationFrameId = requestAnimationFrame(updateProgress);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const onMouseEnter = () => {
|
|
47
|
+
lastFrameTimestamp = null;
|
|
48
|
+
cancelAnimation();
|
|
49
|
+
};
|
|
50
|
+
const onMouseLeave = () => {
|
|
51
|
+
if (!animationFrameId) animationFrameId = requestAnimationFrame(updateProgress);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
messageElement.addEventListener("mouseenter", onMouseEnter);
|
|
55
|
+
messageElement.addEventListener("mouseleave", onMouseLeave);
|
|
56
|
+
messageProgressCleanups.set(messageElement, () => {
|
|
57
|
+
cancelAnimation();
|
|
58
|
+
messageElement.removeEventListener("mouseenter", onMouseEnter);
|
|
59
|
+
messageElement.removeEventListener("mouseleave", onMouseLeave);
|
|
60
|
+
});
|
|
61
|
+
animationFrameId = requestAnimationFrame(updateProgress);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const scheduleReflow = () => {
|
|
65
|
+
if (reflowScheduled) return;
|
|
66
|
+
reflowScheduled = true;
|
|
67
|
+
requestAnimationFrame(() => {
|
|
68
|
+
reflowScheduled = false;
|
|
69
|
+
let offset = MESSAGE_TOP_REM;
|
|
70
|
+
for (const el of activeMessages) {
|
|
71
|
+
el.style.top = `${offset}rem`;
|
|
72
|
+
offset += PxToRem(el.offsetHeight) + MESSAGE_GAP_REM;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const dismissMessage = (el: HTMLElement, callback?: PopupMessageCallback) => {
|
|
78
|
+
const index = activeMessages.indexOf(el);
|
|
79
|
+
if (index === -1) return;
|
|
80
|
+
|
|
81
|
+
stopMessageProgress(el);
|
|
82
|
+
activeMessages.splice(index, 1);
|
|
83
|
+
el.style.right = "-100%";
|
|
84
|
+
el.style.top = "100%";
|
|
85
|
+
RemoveElement(el, DEFAULT_ANIMATION_DURATION, EAnimationClasses.FADE_OUT);
|
|
86
|
+
scheduleReflow();
|
|
87
|
+
callback?.(ECallbackTypes.Destroy);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const _showMessage = (title: string, message: string, messageType: EMessageTypes, delay?: number | UN, duration?: number | UN, icon?: EMessageIcons | UN, callback?: PopupMessageCallback) => {
|
|
91
|
+
if (!IsBrowser()) return;
|
|
92
|
+
|
|
93
|
+
delay = delay ?? 0;
|
|
94
|
+
duration = duration ?? 3000;
|
|
95
|
+
|
|
96
|
+
const createAndShow = () => {
|
|
97
|
+
/** Root message element */
|
|
98
|
+
const messageRootElement = document.createElement("div");
|
|
99
|
+
const messageBodyElement = document.createElement("div");
|
|
100
|
+
const messageHandleElement = document.createElement("div");
|
|
101
|
+
const messageContentElement = document.createElement("div");
|
|
102
|
+
const messageIconElement = document.createElement("div");
|
|
103
|
+
const progressTrackElement = document.createElement("div");
|
|
104
|
+
const progressFillElement = document.createElement("div");
|
|
105
|
+
|
|
106
|
+
document.body.appendChild(messageRootElement);
|
|
107
|
+
messageRootElement.appendChild(messageBodyElement);
|
|
108
|
+
messageBodyElement.appendChild(messageHandleElement);
|
|
109
|
+
messageBodyElement.appendChild(messageIconElement);
|
|
110
|
+
messageBodyElement.appendChild(messageContentElement);
|
|
111
|
+
messageRootElement.appendChild(progressTrackElement);
|
|
112
|
+
progressTrackElement.appendChild(progressFillElement);
|
|
113
|
+
|
|
114
|
+
messageRootElement.className = `${MESSAGE_IDENTITY} pf bs3 r bg1 anim df ddc oh`;
|
|
115
|
+
messageBodyElement.className = "df ac w100";
|
|
116
|
+
messageHandleElement.className = messageType;
|
|
117
|
+
messageIconElement.className = `ib ml4 sm rr ${messageType}`;
|
|
118
|
+
messageContentElement.className = `p4 pr2 fg`;
|
|
119
|
+
progressTrackElement.className = "w100";
|
|
120
|
+
progressFillElement.className = messageType;
|
|
121
|
+
|
|
122
|
+
messageRootElement.style.right = "-100%";
|
|
123
|
+
messageRootElement.style.transitionDuration = `${DEFAULT_ANIMATION_DURATION * 1.5}ms`;
|
|
124
|
+
|
|
125
|
+
messageHandleElement.style.width = "0.3rem";
|
|
126
|
+
messageHandleElement.style.alignSelf = "stretch";
|
|
127
|
+
messageHandleElement.style.flexShrink = "0";
|
|
128
|
+
|
|
129
|
+
progressTrackElement.style.height = `${PROGRESS_BAR_HEIGHT_REM}rem`;
|
|
130
|
+
progressTrackElement.style.flexShrink = "0";
|
|
131
|
+
progressFillElement.style.height = "100%";
|
|
132
|
+
progressFillElement.style.width = "0%";
|
|
133
|
+
|
|
134
|
+
messageIconElement.innerHTML = `<${DEFAULT_ICON_ELEMENT_NAME} class="icon" name="${icon}" />`;
|
|
135
|
+
|
|
136
|
+
const _TM = setTimeout(() => {
|
|
137
|
+
clearTimeout(_TM);
|
|
138
|
+
const WINDOW_WIDTH = window.innerWidth;
|
|
139
|
+
if (WINDOW_WIDTH > 1000) {
|
|
140
|
+
messageRootElement.style.right = "1rem";
|
|
141
|
+
messageRootElement.style.maxWidth = "35rem";
|
|
142
|
+
messageRootElement.style.minWidth = "16rem";
|
|
143
|
+
} else {
|
|
144
|
+
messageRootElement.style.maxWidth = "calc(100vw - 1rem)";
|
|
145
|
+
messageRootElement.style.minWidth = "15rem";
|
|
146
|
+
messageRootElement.style.right = "0.5rem";
|
|
147
|
+
}
|
|
148
|
+
}, 0);
|
|
149
|
+
|
|
150
|
+
const titleElement = document.createElement("p");
|
|
151
|
+
const messageElement = document.createElement("p");
|
|
152
|
+
titleElement.className = "mb1 fw600 w100 df ac";
|
|
153
|
+
messageElement.className = "o08 fs-09 pr3";
|
|
154
|
+
titleElement.innerHTML = `<p class="w100 fg">${title}</p>`;
|
|
155
|
+
messageElement.innerHTML = message;
|
|
156
|
+
messageContentElement.appendChild(titleElement);
|
|
157
|
+
messageContentElement.appendChild(messageElement);
|
|
158
|
+
|
|
159
|
+
const dismiss = () => dismissMessage(messageRootElement, callback);
|
|
160
|
+
CreateIconBox(EIcons.CLOSE, titleElement, null, null, EIconTypes.FILLED, EBackgrounds.Background1, ESizes.VeryTiny, ESizes.Tiny, null, null, null, null, EOpacity.O05, "Clear Message", null, null, null, dismiss);
|
|
161
|
+
|
|
162
|
+
activeMessages.unshift(messageRootElement);
|
|
163
|
+
scheduleReflow();
|
|
164
|
+
requestAnimationFrame(scheduleReflow);
|
|
165
|
+
callback?.(ECallbackTypes.Initialize);
|
|
166
|
+
|
|
167
|
+
if (duration > 0) startMessageProgress(messageRootElement, progressFillElement, duration, dismiss);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (typeof delay === "number" && delay > 0) {
|
|
171
|
+
const _TM = window.setTimeout(() => {
|
|
172
|
+
clearTimeout(_TM);
|
|
173
|
+
createAndShow();
|
|
174
|
+
}, delay);
|
|
175
|
+
} else createAndShow();
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const SuccessMessage = (title: string, message: string, delay?: number | UN, duration?: number | UN, callback?: PopupMessageCallback, icon?: EMessageIcons | UN) => {
|
|
179
|
+
return _showMessage(title, message, EMessageTypes.Success, delay, duration, icon ?? EMessageIcons.Success, callback);
|
|
180
|
+
};
|
|
181
|
+
export const ErrorMessage = (title: string, message: string, delay?: number | UN, duration?: number | UN, callback?: PopupMessageCallback, icon?: EMessageIcons | UN) => {
|
|
182
|
+
return _showMessage(title, message, EMessageTypes.Error, delay, duration, icon ?? EMessageIcons.Error, callback);
|
|
183
|
+
};
|
|
184
|
+
export const WarningMessage = (title: string, message: string, delay?: number | UN, duration?: number | UN, callback?: PopupMessageCallback, icon?: EMessageIcons | UN) => {
|
|
185
|
+
return _showMessage(title, message, EMessageTypes.Warning, delay, duration, icon ?? EMessageIcons.Warning, callback);
|
|
186
|
+
};
|
|
187
|
+
export const InformationMessage = (title: string, message: string, delay?: number | UN, duration?: number | UN, callback?: PopupMessageCallback, icon?: EMessageIcons | UN) => {
|
|
188
|
+
return _showMessage(title, message, EMessageTypes.Information, delay, duration, icon ?? EMessageIcons.Information, callback);
|
|
189
|
+
};
|
|
190
|
+
export const QuestionMessage = (title: string, message: string, delay?: number | UN, duration?: number | UN, callback?: PopupMessageCallback, icon?: EMessageIcons | UN) => {
|
|
191
|
+
return _showMessage(title, message, EMessageTypes.Information, delay, duration, icon ?? EMessageIcons.Question, callback);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
type MessageBoxButton = {
|
|
195
|
+
result: EMessageResult;
|
|
196
|
+
label: string;
|
|
197
|
+
themeBackgroundClass?: EBackgrounds | string;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const normalizeMessageResult = (value: string): EMessageResult => {
|
|
201
|
+
const v = (value ?? "").trim().toLowerCase();
|
|
202
|
+
if (v === "ok") return EMessageResult.OK;
|
|
203
|
+
if (v === "yes") return EMessageResult.Yes;
|
|
204
|
+
if (v === "no") return EMessageResult.No;
|
|
205
|
+
if (v === "cancel") return EMessageResult.Cancel;
|
|
206
|
+
return EMessageResult.Undefined;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const getMessageBoxButtons = (messageButtons: EMessageBoxButtons, themeBackgroundClass: EMessageTypes, overrideYesButtonText?: string | null): MessageBoxButton[] => {
|
|
210
|
+
const parts = (messageButtons ?? EMessageBoxButtons.OK).split("_").filter(Boolean);
|
|
211
|
+
const results = parts.map(normalizeMessageResult).filter((r) => r !== EMessageResult.Undefined);
|
|
212
|
+
const resolved = results.length ? results : [EMessageResult.OK];
|
|
213
|
+
|
|
214
|
+
return resolved.map((result) => {
|
|
215
|
+
if (result === EMessageResult.Yes) return { result, label: overrideYesButtonText ?? "Yes", themeBackgroundClass };
|
|
216
|
+
if (result === EMessageResult.OK) return { result, label: "OK", themeBackgroundClass: themeBackgroundClass };
|
|
217
|
+
if (result === EMessageResult.No) return { result, label: "No", themeBackgroundClass: EBackgrounds.AlwaysDarkBackgroundColor };
|
|
218
|
+
if (result === EMessageResult.Cancel) return { result, label: "Cancel", themeBackgroundClass: EBackgrounds.AlwaysDarkBackgroundColor };
|
|
219
|
+
return { result, label: String(result) };
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const getCloseResult = (buttons: MessageBoxButton[]): EMessageResult => {
|
|
224
|
+
if (buttons.some((b) => b.result === EMessageResult.Cancel)) return EMessageResult.Cancel;
|
|
225
|
+
if (buttons.some((b) => b.result === EMessageResult.No)) return EMessageResult.No;
|
|
226
|
+
if (buttons.some((b) => b.result === EMessageResult.OK)) return EMessageResult.OK;
|
|
227
|
+
return EMessageResult.Undefined;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Windows-like modal message box (DOM-based).
|
|
232
|
+
* - Keyboard: Tab / Shift+Tab / ArrowLeft / ArrowRight to move focus
|
|
233
|
+
* - Enter: activates focused button
|
|
234
|
+
* - Escape: closes with Cancel/No/OK (in that order if present)
|
|
235
|
+
*/
|
|
236
|
+
export const ShowDialogMessageBox = async (messageButtons: EMessageBoxButtons, title: string, message: string, iconName: EMessageIcons, themeBackgroundClass?: EMessageTypes, overrideYesButtonText?: string | null, confirmMessage?: string | null, allowOverlayClick?: boolean): Promise<EMessageResult | null> => {
|
|
237
|
+
if (!IsBrowser()) return null;
|
|
238
|
+
|
|
239
|
+
return new Promise((resolve) => {
|
|
240
|
+
themeBackgroundClass = themeBackgroundClass ?? EMessageTypes.Information;
|
|
241
|
+
const bodyElement = document.body as HTMLBodyElement;
|
|
242
|
+
const overlayElement = document.createElement("div") as HTMLDivElement;
|
|
243
|
+
const dialogElement = document.createElement("div") as HTMLDivElement;
|
|
244
|
+
|
|
245
|
+
const iconBoxElement = document.createElement("div") as HTMLDivElement;
|
|
246
|
+
const headerElement = document.createElement("div") as HTMLDivElement;
|
|
247
|
+
const headerTextElement = document.createElement("div") as HTMLDivElement;
|
|
248
|
+
const titleElement = document.createElement("h4") as HTMLHeadingElement;
|
|
249
|
+
const messageElement = document.createElement("p") as HTMLParagraphElement;
|
|
250
|
+
const confirmElement = document.createElement("p") as HTMLParagraphElement;
|
|
251
|
+
const buttonRowElement = document.createElement("div") as HTMLDivElement;
|
|
252
|
+
|
|
253
|
+
const buttons = getMessageBoxButtons(messageButtons, themeBackgroundClass, overrideYesButtonText);
|
|
254
|
+
const actionButtons: HTMLButtonElement[] = [];
|
|
255
|
+
let currentFocusIndex = 0;
|
|
256
|
+
let isClosed = false;
|
|
257
|
+
|
|
258
|
+
overlayElement.className = `pf df ac jc ${EBackgrounds.PopupBackgroundColor} ${EAnimationClasses.FADE_IN}`;
|
|
259
|
+
overlayElement.style.zIndex = "100000";
|
|
260
|
+
overlayElement.style.inset = "0";
|
|
261
|
+
|
|
262
|
+
dialogElement.className = `bg1 r bs3 anim df ddc`;
|
|
263
|
+
dialogElement.style.maxWidth = "36rem";
|
|
264
|
+
dialogElement.style.width = "calc(100vw - 2rem)";
|
|
265
|
+
dialogElement.style.boxSizing = "border-box";
|
|
266
|
+
|
|
267
|
+
headerElement.className = "df w100";
|
|
268
|
+
headerElement.style.gap = "1rem";
|
|
269
|
+
headerElement.style.padding = "1.25rem";
|
|
270
|
+
|
|
271
|
+
iconBoxElement.className = `df ac jc rr bb ${themeBackgroundClass}`;
|
|
272
|
+
iconBoxElement.style.minWidth = "4rem";
|
|
273
|
+
iconBoxElement.style.maxWidth = "4rem";
|
|
274
|
+
iconBoxElement.style.minHeight = "4rem";
|
|
275
|
+
iconBoxElement.style.maxHeight = "4rem";
|
|
276
|
+
|
|
277
|
+
const iconElement = document.createElement(DEFAULT_ICON_ELEMENT_NAME) as HTMLElement & { name?: string };
|
|
278
|
+
iconElement.name = `${iconName}${EIconTypes.OUTLINED}`;
|
|
279
|
+
iconElement.className = `icon ${ESizes.Large} o08`;
|
|
280
|
+
iconElement.style.color = "white";
|
|
281
|
+
iconBoxElement.appendChild(iconElement);
|
|
282
|
+
|
|
283
|
+
headerTextElement.className = "fg";
|
|
284
|
+
|
|
285
|
+
titleElement.className = "fw600";
|
|
286
|
+
titleElement.style.margin = "0";
|
|
287
|
+
titleElement.innerText = title ?? "";
|
|
288
|
+
|
|
289
|
+
messageElement.className = "o08";
|
|
290
|
+
messageElement.style.margin = "0.5rem 0 0 0";
|
|
291
|
+
messageElement.innerHTML = message ?? "";
|
|
292
|
+
|
|
293
|
+
confirmElement.className = "o08";
|
|
294
|
+
confirmElement.style.margin = "0.75rem 0 0 0";
|
|
295
|
+
confirmElement.innerHTML = confirmMessage ?? "";
|
|
296
|
+
if (!confirmMessage) confirmElement.style.display = "none";
|
|
297
|
+
|
|
298
|
+
buttonRowElement.className = "df w100";
|
|
299
|
+
buttonRowElement.style.gap = "0.75rem";
|
|
300
|
+
buttonRowElement.style.justifyContent = "flex-end";
|
|
301
|
+
buttonRowElement.style.padding = "0 1.25rem 1.25rem 1.25rem";
|
|
302
|
+
|
|
303
|
+
const setFocus = (index: number) => {
|
|
304
|
+
currentFocusIndex = Math.min(Math.max(index, 0), actionButtons.length - 1);
|
|
305
|
+
actionButtons[currentFocusIndex]?.focus();
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const closeDialog = (result: EMessageResult) => {
|
|
309
|
+
if (isClosed) return;
|
|
310
|
+
isClosed = true;
|
|
311
|
+
|
|
312
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
313
|
+
overlayElement.removeEventListener("click", onOverlayClick);
|
|
314
|
+
|
|
315
|
+
overlayElement.classList.remove(EAnimationClasses.FADE_IN);
|
|
316
|
+
overlayElement.classList.add(EAnimationClasses.FADE_OUT);
|
|
317
|
+
dialogElement.classList.add(EAnimationClasses.PULSE_OUT);
|
|
318
|
+
|
|
319
|
+
RemoveElement(overlayElement, DEFAULT_ANIMATION_DURATION, EAnimationClasses.FADE_OUT).finally(() => resolve(result));
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const onOverlayClick = (event: MouseEvent) => {
|
|
323
|
+
if (event.target === overlayElement) closeDialog(getCloseResult(buttons));
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
327
|
+
const key = event.key;
|
|
328
|
+
const isShiftTab = key === "Tab" && event.shiftKey;
|
|
329
|
+
const isTab = key === "Tab" && !event.shiftKey;
|
|
330
|
+
|
|
331
|
+
if (key === "ArrowLeft" || isShiftTab) {
|
|
332
|
+
event.preventDefault();
|
|
333
|
+
setFocus((currentFocusIndex - 1 + actionButtons.length) % actionButtons.length);
|
|
334
|
+
} else if (key === "ArrowRight" || isTab) {
|
|
335
|
+
event.preventDefault();
|
|
336
|
+
setFocus((currentFocusIndex + 1) % actionButtons.length);
|
|
337
|
+
} else if (key === "Enter") {
|
|
338
|
+
event.preventDefault();
|
|
339
|
+
const result = buttons[currentFocusIndex]?.result ?? EMessageResult.Undefined;
|
|
340
|
+
closeDialog(result);
|
|
341
|
+
} else if (key === "Escape") {
|
|
342
|
+
event.preventDefault();
|
|
343
|
+
closeDialog(getCloseResult(buttons));
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const createButton = (button: MessageBoxButton) => {
|
|
348
|
+
const buttonElement = document.createElement("button") as HTMLButtonElement;
|
|
349
|
+
buttonElement.type = "button";
|
|
350
|
+
buttonElement.className = `sn r h p2 pr4 pl4 ${button.themeBackgroundClass}`;
|
|
351
|
+
buttonElement.style.minWidth = "5.5rem";
|
|
352
|
+
buttonElement.innerText = button.label;
|
|
353
|
+
|
|
354
|
+
const onClick = () => closeDialog(button.result);
|
|
355
|
+
buttonElement.addEventListener("click", onClick, { once: true });
|
|
356
|
+
return buttonElement;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
for (const button of buttons) {
|
|
360
|
+
const buttonElement = createButton(button);
|
|
361
|
+
buttonRowElement.appendChild(buttonElement);
|
|
362
|
+
actionButtons.push(buttonElement);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
headerTextElement.appendChild(titleElement);
|
|
366
|
+
headerTextElement.appendChild(messageElement);
|
|
367
|
+
headerTextElement.appendChild(confirmElement);
|
|
368
|
+
|
|
369
|
+
headerElement.appendChild(iconBoxElement);
|
|
370
|
+
headerElement.appendChild(headerTextElement);
|
|
371
|
+
|
|
372
|
+
dialogElement.appendChild(headerElement);
|
|
373
|
+
dialogElement.appendChild(buttonRowElement);
|
|
374
|
+
|
|
375
|
+
overlayElement.appendChild(dialogElement);
|
|
376
|
+
bodyElement.insertBefore(overlayElement, bodyElement.firstChild);
|
|
377
|
+
|
|
378
|
+
if (allowOverlayClick === false) overlayElement.addEventListener("click", onOverlayClick);
|
|
379
|
+
document.addEventListener("keydown", onKeyDown);
|
|
380
|
+
|
|
381
|
+
setTimeout(() => setFocus(0), 0);
|
|
382
|
+
});
|
|
383
|
+
};
|