nexus-shared 1.1.4 → 1.1.6

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.
Files changed (95) hide show
  1. package/package.json +16 -40
  2. package/src/api-services/preference-service.tsx +5 -0
  3. package/src/api-services/system-service.tsx +322 -0
  4. package/src/components/documents/button.tsx +136 -0
  5. package/src/components/documents/icon-box.tsx +92 -0
  6. package/src/components/documents/page-title.tsx +7 -0
  7. package/src/components/documents/tab-button.tsx +169 -0
  8. package/src/components/index.js +0 -0
  9. package/src/components/inputs/checkbox-input.tsx +66 -0
  10. package/src/components/inputs/input-box.tsx +45 -0
  11. package/src/components/inputs/input-element.tsx +65 -0
  12. package/src/components/inputs/input-form.tsx +50 -0
  13. package/src/components/inputs/input.tsx +181 -0
  14. package/src/components/inputs/number-input.tsx +108 -0
  15. package/src/components/inputs/radiobox-input.tsx +53 -0
  16. package/src/components/inputs/textarea-input.tsx +47 -0
  17. package/src/components/inputs/textbox-input.tsx +45 -0
  18. package/src/components/layouts/global-dialogbox.tsx +433 -0
  19. package/src/components/layouts/global-layout.tsx +63 -0
  20. package/src/components/layouts/layout-helpers.tsx +23 -0
  21. package/src/components/layouts/utility-menu.tsx +71 -0
  22. package/src/components/panels/theme-panel.tsx +44 -0
  23. package/src/helpers/bitwise-helpers.tsx +11 -0
  24. package/src/helpers/browser-helpers.tsx +73 -0
  25. package/src/helpers/datasource-helpers.tsx +99 -0
  26. package/src/helpers/element-helpers.tsx +57 -0
  27. package/src/helpers/input-helpers.tsx +24 -0
  28. package/src/helpers/string-helpers.tsx +28 -0
  29. package/src/helpers/utility-helpers.tsx +44 -0
  30. package/src/index.ts +0 -11
  31. package/src/interfaces/browser-interfaces.tsx +23 -0
  32. package/src/interfaces/button-interfaces.tsx +63 -0
  33. package/src/interfaces/datasource-interfaces.tsx +22 -0
  34. package/src/interfaces/datatable-interfaces.tsx +25 -0
  35. package/src/interfaces/dialogbox-interfaces.tsx +5 -0
  36. package/src/interfaces/http-interfaces.tsx +15 -0
  37. package/src/interfaces/icon-interfaces.tsx +126 -0
  38. package/src/interfaces/input-interfaces.tsx +360 -0
  39. package/src/interfaces/layout-interfaces.tsx +191 -0
  40. package/src/interfaces/menu-interfaces.tsx +36 -0
  41. package/src/interfaces/permission-interfaces.tsx +9 -0
  42. package/src/interfaces/storage-interfaces.tsx +3 -0
  43. package/src/interfaces/system-interfaces.tsx +22 -0
  44. package/src/interfaces/theme-interfaces.tsx +209 -0
  45. package/src/interfaces/type-interfaces.tsx +22 -0
  46. package/src/nexus-client.tsx +23 -0
  47. package/src/nexus.environments.tsx +34 -0
  48. package/src/services/loader-service.tsx +168 -0
  49. package/src/services/localstorage-service.tsx +45 -0
  50. package/src/services/theme-service.tsx +149 -0
  51. package/src/styles/nexus.animation.css +269 -0
  52. package/src/styles/nexus.core.css +119 -0
  53. package/src/styles/nexus.dialog.css +141 -0
  54. package/src/styles/nexus.icon.css +50 -0
  55. package/src/styles/nexus.input.css +207 -0
  56. package/src/styles/nexus.loader.css +11 -0
  57. package/src/styles/nexus.logic.css +18 -0
  58. package/src/styles/nexus.utility.css +347 -0
  59. package/dist/chunk-7GVFDWOS.js +0 -28
  60. package/dist/chunk-7GVFDWOS.js.map +0 -1
  61. package/dist/chunk-EW6K4PYI.js +0 -96
  62. package/dist/chunk-EW6K4PYI.js.map +0 -1
  63. package/dist/chunk-UMV7E2RN.js +0 -1
  64. package/dist/chunk-UMV7E2RN.js.map +0 -1
  65. package/dist/client.css +0 -119
  66. package/dist/client.css.map +0 -1
  67. package/dist/client.d.ts +0 -9
  68. package/dist/client.js +0 -7
  69. package/dist/client.js.map +0 -1
  70. package/dist/index.css +0 -207
  71. package/dist/index.css.map +0 -1
  72. package/dist/index.d.ts +0 -5
  73. package/dist/index.js +0 -12
  74. package/dist/index.js.map +0 -1
  75. package/dist/interface.d.ts +0 -9
  76. package/dist/interface.js +0 -2
  77. package/dist/interface.js.map +0 -1
  78. package/dist/nexus-list-DV45tcM0.d.ts +0 -24
  79. package/dist/server.css +0 -88
  80. package/dist/server.css.map +0 -1
  81. package/dist/server.d.ts +0 -9
  82. package/dist/server.js +0 -7
  83. package/dist/server.js.map +0 -1
  84. package/src/client/index.ts +0 -1
  85. package/src/client/nexus-selectable-list.css +0 -131
  86. package/src/client/nexus-selectable-list.tsx +0 -111
  87. package/src/client.ts +0 -7
  88. package/src/interface.ts +0 -5
  89. package/src/interfaces/index.ts +0 -6
  90. package/src/interfaces/nexus-base.ts +0 -5
  91. package/src/interfaces/nexus-list.ts +0 -24
  92. package/src/server/index.ts +0 -1
  93. package/src/server/nexus-stat-list.css +0 -92
  94. package/src/server/nexus-stat-list.tsx +0 -46
  95. package/src/server.ts +0 -8
@@ -0,0 +1,108 @@
1
+ import { GetInputId, SetHaveValueClass } from "../../helpers/input-helpers";
2
+ import { IsNullOrEmpty } from "../../helpers/string-helpers";
3
+ import { IInputCreateResults, INumber } from "../../interfaces/input-interfaces";
4
+ import { CreateInput } from "./input";
5
+ import { CreateInputBox, CreateInputWrapper } from "./input-box";
6
+
7
+ function getDecimalPlaces(param: INumber): number {
8
+ const fp = param.floatingPoint;
9
+ if (fp === undefined || fp === null || fp < 0) return 0;
10
+ return Math.floor(fp);
11
+ }
12
+
13
+ function roundToDecimalPlaces(value: number, decimalPlaces: number): number {
14
+ if (decimalPlaces <= 0) return Math.round(value);
15
+ const factor = Math.pow(10, decimalPlaces);
16
+ return Math.round(value * factor) / factor;
17
+ }
18
+
19
+ function parseNumber(raw: string): number | null {
20
+ const trimmed = raw.trim();
21
+ if (trimmed === "") return null;
22
+ const parsed = Number(trimmed);
23
+ return Number.isFinite(parsed) ? parsed : null;
24
+ }
25
+
26
+ function adjustValue(value: number, raw: string, param: INumber, decimalPlaces: number, forceClamp = false): number {
27
+ let result = roundToDecimalPlaces(value, decimalPlaces);
28
+ if (!param.isAutoAdjust) return result;
29
+
30
+ const inputLength = raw.trim().length;
31
+
32
+ if (param.minLength !== undefined && param.minLength !== null) {
33
+ const minLength = String(param.minLength).length;
34
+ if ((forceClamp || inputLength >= minLength) && result < param.minLength) result = param.minLength;
35
+ }
36
+ if (param.maxLength !== undefined && param.maxLength !== null) {
37
+ const maxLength = String(param.maxLength).length;
38
+ if ((forceClamp || inputLength >= maxLength) && result > param.maxLength) result = param.maxLength;
39
+ }
40
+ return result;
41
+ }
42
+
43
+ export const CreateNumber = (param: INumber, parentElement: HTMLElement | null, isTabular: boolean = false) => {
44
+ const ELEMENT_ID = GetInputId(param);
45
+ const hasPlaceholder = !IsNullOrEmpty(param.placeholder);
46
+ const decimalPlaces = getDecimalPlaces(param);
47
+
48
+ const input = CreateInput(param) as HTMLInputElement;
49
+ const box = CreateInputBox(param, input, null, isTabular);
50
+ const wrapper = isTabular ? box : CreateInputWrapper(param, box, parentElement);
51
+
52
+ function handleChanged(val: number | undefined | null, isInit: boolean, isForced: boolean, isDisposed: boolean) {
53
+ param.onChanged?.(val ?? null, isInit, isForced, isDisposed, ELEMENT_ID, undefined);
54
+ SetHaveValueClass(input, (val !== undefined && val !== null) || hasPlaceholder);
55
+ }
56
+
57
+ function forceChangeDefaultValue(defaultValue: number | null): boolean {
58
+ if (defaultValue === null || defaultValue === undefined) {
59
+ input.value = "";
60
+ handleChanged(null, false, true, false);
61
+ return true;
62
+ }
63
+ const adjusted = adjustValue(defaultValue, defaultValue.toString(), param, decimalPlaces, true);
64
+ input.value = adjusted.toString();
65
+ handleChanged(adjusted, false, true, false);
66
+ return true;
67
+ }
68
+
69
+ function onInputChanged() {
70
+ if (param.isAutoAdjust) return;
71
+ const parsed = parseNumber(input.value);
72
+ handleChanged(parsed, false, false, false);
73
+ }
74
+
75
+ function onBlur() {
76
+ if (!param.isAutoAdjust) return;
77
+ const raw = input.value;
78
+ const parsed = parseNumber(raw);
79
+ if (parsed === null) {
80
+ handleChanged(null, false, false, false);
81
+ return;
82
+ }
83
+ const adjusted = adjustValue(parsed, raw, param, decimalPlaces, true);
84
+ input.value = adjusted.toString();
85
+ handleChanged(adjusted, false, false, false);
86
+ }
87
+
88
+ if (!param.isAutoAdjust) input.addEventListener("input", onInputChanged);
89
+ if (param.isAutoAdjust) input.addEventListener("blur", onBlur);
90
+
91
+ // Keep initial display as loaded from DB/default — do not round or clamp on init.
92
+ handleChanged(param.defaultValue ?? null, true, false, false);
93
+
94
+ return {
95
+ input: param,
96
+ inputElement: input,
97
+ rootElement: wrapper,
98
+ forceChangeDefaultValue: forceChangeDefaultValue,
99
+ dispose: function (invokeOnChanged?: boolean) {
100
+ if (invokeOnChanged !== false) handleChanged(null, false, true, true);
101
+ input.removeEventListener("input", onInputChanged);
102
+ input.removeEventListener("blur", onBlur);
103
+ input.remove();
104
+ box.remove();
105
+ wrapper.remove();
106
+ },
107
+ } as IInputCreateResults;
108
+ };
@@ -0,0 +1,53 @@
1
+ import { GetInputId, SetHaveValueClass } from "../../helpers/input-helpers";
2
+ import { IInputCreateResults, IRadiobox } from "../../interfaces/input-interfaces";
3
+ import { CreateInput } from "./input";
4
+ import { CreateInputBox, CreateInputWrapper } from "./input-box";
5
+
6
+ export const CreateRadiobox = (param: IRadiobox, parentElement: HTMLElement | null, isTabular: boolean, isCheckbox: boolean = false) => {
7
+ const input = CreateInput(param) as HTMLInputElement;
8
+ const ELEMENT_ID = GetInputId(param);
9
+
10
+ input.classList.add("pr");
11
+ input.classList.remove("ipt");
12
+ input.defaultChecked = param.defaultValue ?? false;
13
+ input.type = "radio";
14
+ if (isCheckbox) input.classList.add("checkbox");
15
+
16
+ const box = CreateInputBox(param, input, null, isTabular);
17
+ const label = box.querySelector("label") as HTMLLabelElement;
18
+ if (label) label.className = "p2 ilb hc o08";
19
+ const wrapper = isTabular ? box : CreateInputWrapper(param, box, parentElement);
20
+
21
+ box.classList.add("df", "ac", "pl3", "ipt-bxh");
22
+ box.classList.remove("ipt-bx");
23
+
24
+ function handleChanged(val: boolean, isInit: boolean, isForced: boolean, isDisposed: boolean) {
25
+ if (!val) return;
26
+
27
+ param.onChanged?.(param.checkedValue, isInit, isForced, isDisposed, ELEMENT_ID, undefined);
28
+ }
29
+ function forceChangeDefaultValue(defaultValue: boolean): boolean {
30
+ input.value = defaultValue.toString();
31
+ handleChanged(defaultValue, false, true, false);
32
+ return true;
33
+ }
34
+ function onInputChanged(e: Event) {
35
+ handleChanged(input.checked, false, false, false);
36
+ }
37
+ input.addEventListener("input", onInputChanged);
38
+ SetHaveValueClass(input, true);
39
+ handleChanged(input.checked, true, false, false);
40
+ return {
41
+ input: param,
42
+ inputElement: input,
43
+ rootElement: wrapper,
44
+ forceChangeDefaultValue: forceChangeDefaultValue,
45
+ dispose: function (invokeOnChanged?: boolean) {
46
+ if (invokeOnChanged !== false) handleChanged(param.defaultValue ?? false, false, true, true);
47
+ input.removeEventListener("input", onInputChanged);
48
+ input.remove();
49
+ box.remove();
50
+ wrapper.remove();
51
+ },
52
+ } as IInputCreateResults;
53
+ };
@@ -0,0 +1,47 @@
1
+ import { GetInputId, SetHaveValueClass } from "../../helpers/input-helpers";
2
+ import { IsNullOrEmpty } from "../../helpers/string-helpers";
3
+ import { IInputCreateResults, ITextarea } from "../../interfaces/input-interfaces";
4
+ import { CreateInput } from "./input";
5
+ import { CreateInputBox, CreateInputWrapper } from "./input-box";
6
+
7
+ export const CreateTextarea = (param: ITextarea, parentElement: HTMLElement | null, isTabular: boolean = false) => {
8
+ const ELEMENT_ID = GetInputId(param);
9
+ const hasPlaceholder = !IsNullOrEmpty(param.placeholder);
10
+
11
+ const input = CreateInput(param) as HTMLTextAreaElement;
12
+ const box = CreateInputBox(param, input, null, isTabular);
13
+ const wrapper = isTabular ? box : CreateInputWrapper(param, box, parentElement);
14
+
15
+ box.classList.add("textarea-box");
16
+
17
+ function handleChanged(val: string | undefined | null, isInit: boolean, isForced: boolean, isDisposed: boolean) {
18
+ const value = val?.trim() ?? null;
19
+ const isNullorEmpty = IsNullOrEmpty(value);
20
+ param.onChanged?.(isNullorEmpty ? null : value, isInit, isForced, isDisposed, ELEMENT_ID, undefined);
21
+ SetHaveValueClass(input, !isNullorEmpty || hasPlaceholder);
22
+ }
23
+
24
+ function forceChangeDefaultValue(defaultValue: string | null): boolean {
25
+ input.value = defaultValue ?? "";
26
+ handleChanged(defaultValue, false, true, false);
27
+ return true;
28
+ }
29
+ function onInputChanged(e: Event) {
30
+ handleChanged((e.currentTarget as HTMLInputElement).value, false, false, false);
31
+ }
32
+ input.addEventListener("input", onInputChanged);
33
+ handleChanged(param.defaultValue, true, false, false);
34
+ return {
35
+ input: param,
36
+ inputElement: input,
37
+ rootElement: wrapper,
38
+ forceChangeDefaultValue: forceChangeDefaultValue,
39
+ dispose: function (invokeOnChanged?: boolean) {
40
+ if (invokeOnChanged !== false) handleChanged(null, false, true, true);
41
+ input.removeEventListener("input", onInputChanged);
42
+ input.remove();
43
+ box.remove();
44
+ wrapper.remove();
45
+ },
46
+ } as IInputCreateResults;
47
+ };
@@ -0,0 +1,45 @@
1
+ import { GetInputId, SetHaveValueClass } from "../../helpers/input-helpers";
2
+ import { IsNullOrEmpty } from "../../helpers/string-helpers";
3
+ import { IInputCreateResults, ITextbox } from "../../interfaces/input-interfaces";
4
+ import { CreateInput } from "./input";
5
+ import { CreateInputBox, CreateInputWrapper } from "./input-box";
6
+
7
+ export const CreateTextbox = (param: ITextbox, parentElement: HTMLElement | null, isTabular: boolean = false) => {
8
+ const ELEMENT_ID = GetInputId(param);
9
+ const hasPlaceholder = !IsNullOrEmpty(param.placeholder);
10
+
11
+ const input = CreateInput(param);
12
+ const box = CreateInputBox(param, input, null, isTabular);
13
+ const wrapper = isTabular ? box : CreateInputWrapper(param, box, parentElement);
14
+
15
+ function handleChanged(val: string | undefined | null, isInit: boolean, isForced: boolean, isDisposed: boolean) {
16
+ const value = val?.trim() ?? null;
17
+ const isNullorEmpty = IsNullOrEmpty(value);
18
+ param.onChanged?.(isNullorEmpty ? null : value, isInit, isForced, isDisposed, ELEMENT_ID, undefined);
19
+ SetHaveValueClass(input, !isNullorEmpty || hasPlaceholder);
20
+ }
21
+
22
+ function forceChangeDefaultValue(defaultValue: string | null): boolean {
23
+ input.value = defaultValue ?? "";
24
+ handleChanged(defaultValue, false, true, false);
25
+ return true;
26
+ }
27
+ function onInputChanged(e: Event) {
28
+ handleChanged((e.currentTarget as HTMLInputElement).value, false, false, false);
29
+ }
30
+ input.addEventListener("input", onInputChanged);
31
+ handleChanged(param.defaultValue, true, false, false);
32
+ return {
33
+ input: param,
34
+ inputElement: input,
35
+ rootElement: wrapper,
36
+ forceChangeDefaultValue: forceChangeDefaultValue,
37
+ dispose: function (invokeOnChanged?: boolean) {
38
+ if (invokeOnChanged !== false) handleChanged(null, false, true, true);
39
+ input.removeEventListener("input", onInputChanged);
40
+ input.remove();
41
+ box.remove();
42
+ wrapper.remove();
43
+ },
44
+ } as IInputCreateResults;
45
+ };
@@ -0,0 +1,433 @@
1
+ import React, { useEffect } from "react";
2
+ import { ConvertRemToPixels, IsKeyPressed } from "../../helpers/browser-helpers";
3
+ import { Debounce, DebouncedFunction } from "../../helpers/utility-helpers";
4
+ import { EKeys } from "../../interfaces/browser-interfaces";
5
+ import { IDialogboxResults } from "../../interfaces/dialogbox-interfaces";
6
+ import { DEFAULT_ANIMATION_DURATION, EPositions } from "../../interfaces/layout-interfaces";
7
+ import { EBackgrounds } from "../../interfaces/theme-interfaces";
8
+ import { UN } from "../../interfaces/type-interfaces";
9
+ import { NEXUS_CONFIG } from "../../nexus.environments";
10
+
11
+ const GetDialogId = (buttonIdentity: string) => `dialog-${buttonIdentity}`;
12
+ const VISIBILITY_KEY = "data-dialog-visibility";
13
+ export const DEFAULT_DIALOGBOX_HOVER_EFFECT_DELAY = 500;
14
+
15
+ interface IDialogRegistryEntry {
16
+ triggerElement: HTMLElement;
17
+ affectedElement: HTMLElement;
18
+ iggnoreChildClick?: boolean;
19
+ close: (isInitial: boolean) => void;
20
+ }
21
+
22
+ /** All initialized dialogs — O(1) lookup by trigger. */
23
+ const dialogRegistry = new Map<HTMLElement, IDialogRegistryEntry>();
24
+ /** Only open dialogs — hot paths iterate this (usually 1–2), not all 100+ instances. */
25
+ const openDialogs = new Set<HTMLElement>();
26
+
27
+ let globalDocumentClickAttached = false;
28
+ let globalDocumentClickTimeout: ReturnType<typeof setTimeout>;
29
+
30
+ const shouldKeepDialogOpen = (entry: IDialogRegistryEntry, path: EventTarget[], target: HTMLElement) => {
31
+ if (entry.iggnoreChildClick === false) return target === entry.triggerElement;
32
+
33
+ return (
34
+ path.includes(entry.triggerElement) ||
35
+ path.includes(entry.affectedElement) ||
36
+ isClickInsideOpenNestedDialog(entry.triggerElement, path) ||
37
+ entry.triggerElement.contains(target)
38
+ );
39
+ };
40
+
41
+ const isClickInsideOpenNestedDialog = (parentTrigger: HTMLElement, path: EventTarget[]) => {
42
+ for (const trigger of openDialogs) {
43
+ if (trigger === parentTrigger) continue;
44
+ const nested = dialogRegistry.get(trigger);
45
+ if (!nested || !parentTrigger.contains(nested.triggerElement)) continue;
46
+ if (path.includes(nested.triggerElement) || path.includes(nested.affectedElement)) return true;
47
+ }
48
+ return false;
49
+ };
50
+
51
+ const onGlobalDocumentClick = (e: MouseEvent) => {
52
+ if (openDialogs.size === 0) return;
53
+
54
+ const path = e.composedPath();
55
+ const target = e.target as HTMLElement;
56
+
57
+ for (const trigger of [...openDialogs]) {
58
+ const entry = dialogRegistry.get(trigger);
59
+ if (!entry || shouldKeepDialogOpen(entry, path, target)) continue;
60
+ entry.close(false);
61
+ }
62
+ };
63
+
64
+ const scheduleGlobalDocumentClick = () => {
65
+ clearTimeout(globalDocumentClickTimeout);
66
+ globalDocumentClickTimeout = setTimeout(() => {
67
+ if (openDialogs.size === 0) return;
68
+ if (!globalDocumentClickAttached) {
69
+ document.addEventListener("click", onGlobalDocumentClick);
70
+ globalDocumentClickAttached = true;
71
+ }
72
+ }, 0);
73
+ };
74
+
75
+ const teardownGlobalDocumentClick = () => {
76
+ if (openDialogs.size > 0) return;
77
+ clearTimeout(globalDocumentClickTimeout);
78
+ document.removeEventListener("click", onGlobalDocumentClick);
79
+ globalDocumentClickAttached = false;
80
+ };
81
+
82
+ /** Keep parent panels open when opening a nested dialog (e.g. theme overflow ellipsis inside utility menu). */
83
+ const closeOtherDialogs = (currentTrigger: HTMLElement, isInitial: boolean) => {
84
+ for (const trigger of openDialogs) {
85
+ if (trigger === currentTrigger) continue;
86
+ const entry = dialogRegistry.get(trigger);
87
+ if (!entry) continue;
88
+ if (entry.triggerElement.contains(currentTrigger)) continue;
89
+ entry.close(isInitial);
90
+ }
91
+ };
92
+
93
+ const closeNestedDialogs = (parentTrigger: HTMLElement, isInitial: boolean) => {
94
+ for (const trigger of openDialogs) {
95
+ if (trigger === parentTrigger) continue;
96
+ const entry = dialogRegistry.get(trigger);
97
+ if (!entry || !parentTrigger.contains(entry.triggerElement)) continue;
98
+ entry.close(isInitial);
99
+ }
100
+ };
101
+
102
+ const registerDialog = (entry: IDialogRegistryEntry) => {
103
+ dialogRegistry.set(entry.triggerElement, entry);
104
+ };
105
+
106
+ const unregisterDialog = (triggerElement: HTMLElement) => {
107
+ openDialogs.delete(triggerElement);
108
+ dialogRegistry.delete(triggerElement);
109
+ teardownGlobalDocumentClick();
110
+ };
111
+
112
+ /**
113
+ *
114
+ * @param children
115
+ * @param position
116
+ * @param buttonIdentity
117
+ * @param background
118
+ * @param defaultWidth The default width of the dialog box (in REM units).
119
+ * @param defaultPadding
120
+ * @param iggnoreChildClick
121
+ * @param isFixedHeight
122
+ * @param defaultClass
123
+ * @param preferTopWhenEnoughSpace
124
+ *
125
+ * @returns
126
+ */
127
+ interface IDialogboxProps {
128
+ children: React.ReactNode;
129
+ hoverEffectDelay?: number | boolean;
130
+ position?: EPositions;
131
+ buttonIdentity: string;
132
+ background?: EBackgrounds;
133
+ defaultWidth?: number;
134
+ defaultPadding?: number;
135
+ autoAdjuctPosition?: boolean;
136
+ iggnoreChildClick?: boolean;
137
+ isFixedHeight?: boolean;
138
+ preferTopWhenEnoughSpace?: boolean;
139
+ defaultClass?: string;
140
+ }
141
+
142
+ export const GlobalDialogbox = ({ children, hoverEffectDelay, position, buttonIdentity, background, defaultWidth, defaultPadding, iggnoreChildClick, isFixedHeight, defaultClass, preferTopWhenEnoughSpace }: Readonly<IDialogboxProps>) => {
143
+ const [isVisible, setIsVisible] = React.useState<boolean>(false);
144
+
145
+ const onVisibilityChanged = (visible: boolean) => {
146
+ setIsVisible(visible);
147
+ };
148
+ const onDisposed = () => {};
149
+
150
+ useEffect(() => {
151
+ let dialogResults: IDialogboxResults | null = null;
152
+ const timeout = setTimeout(() => {
153
+ clearTimeout(timeout);
154
+ const button = document.getElementById(buttonIdentity);
155
+ const dialog = document.getElementById(GetDialogId(buttonIdentity));
156
+ if (!button || !dialog) return;
157
+
158
+ const defaultHeight = isFixedHeight ? "80vh" : undefined;
159
+ dialogResults = InitializeDialogbox(button, dialog, hoverEffectDelay, defaultHeight, defaultPadding, defaultWidth, background, onVisibilityChanged, onDisposed, iggnoreChildClick, position, defaultClass, preferTopWhenEnoughSpace, null);
160
+ }, 0);
161
+ return () => {
162
+ dialogResults?.destroy();
163
+ clearTimeout(timeout);
164
+ };
165
+ }, []);
166
+
167
+ return <ul id={GetDialogId(buttonIdentity)}>{isVisible && children}</ul>;
168
+ };
169
+
170
+ export const InitializeDialogbox = (triggerElement: HTMLElement, affectedElement: HTMLElement, hoverEffectDelay?: number | boolean, defaultHeight?: string, defaultPadding?: number, defaultWidth?: number, background?: EBackgrounds, onVisibilityChanged?: (isVisible: boolean) => void, onDisposed?: () => void, iggnoreChildClick?: boolean, position?: EPositions, defaultClass?: string, preferTopWhenEnoughSpace?: boolean, targetInput?: HTMLTextAreaElement | HTMLInputElement | UN, isAutoHideInInitialization?: boolean): IDialogboxResults | null => {
171
+ if (!triggerElement || !affectedElement) return null;
172
+ defaultHeight = defaultHeight ?? "auto";
173
+ defaultPadding = defaultPadding ?? 5;
174
+ defaultWidth = defaultWidth ?? 34;
175
+
176
+ const isTargetForInput: boolean = targetInput ? true : false;
177
+
178
+ const forceUpdate = (index: number) => {
179
+ if (index === 0) {
180
+ affectedElement.classList.remove("anim", "dn");
181
+ } else if (index === 1) {
182
+ affectedElement.classList.add("dn");
183
+ affectedElement.classList.remove("anim");
184
+ }
185
+ };
186
+ const forceUpdateDebounced = Debounce(forceUpdate, 300);
187
+ const refreshDialog = () => {
188
+ const dialog = affectedElement;
189
+ const parentElement = triggerElement;
190
+
191
+ if (!parentElement) return;
192
+
193
+ const inputRec = parentElement.getBoundingClientRect();
194
+ const boxRec = dialog.getBoundingClientRect();
195
+ const style = window.getComputedStyle(parentElement);
196
+ const inputHeight = inputRec.height + parseFloat(style.borderBottomWidth) + parseFloat(style.borderTopWidth);
197
+ const top = inputRec.top + inputHeight;
198
+ const windowHeight = window.innerHeight;
199
+ const bottomOverflow = top + boxRec.height;
200
+ dialog.classList.remove("T");
201
+
202
+ const topSpace = inputRec.top - boxRec.height;
203
+ const bottomSpace = windowHeight - (inputRec.top + inputHeight + boxRec.height);
204
+
205
+ if (!isTargetForInput) {
206
+ if (preferTopWhenEnoughSpace && topSpace > 0 && bottomSpace > 0) {
207
+ // Both sides have space, prefer top
208
+ dialog.classList.add("T");
209
+ dialog.style.top = `${top - inputHeight - boxRec.height}px`;
210
+ } else if (bottomOverflow > windowHeight) {
211
+ // Bottom overflow
212
+ if (top - inputHeight - boxRec.height > 0) {
213
+ dialog.classList.add("T");
214
+ dialog.style.top = `${top - inputHeight - boxRec.height}px`;
215
+ } else {
216
+ // If no space on top either, push to bottom of screen
217
+ dialog.style.top = `${windowHeight - boxRec.height}px`;
218
+ }
219
+ } else {
220
+ // Default to bottom
221
+ dialog.style.top = `${top}px`;
222
+ }
223
+ }
224
+
225
+ // REM to PX conversion
226
+
227
+ const DEFAULT_PADDING = ConvertRemToPixels(1);
228
+
229
+ if (position === EPositions.Right) dialog.style.left = `${inputRec.right - boxRec.width}px`;
230
+ else if (position === EPositions.Left) dialog.style.left = `${inputRec.left}px`;
231
+ else if (position === EPositions.Center) dialog.style.left = `${inputRec.left + inputRec.width / 2 - boxRec.width / 2}px`;
232
+
233
+ // Handle viewport overflow for dialogs without explicit position
234
+ if (!position) {
235
+ if (inputRec.left + boxRec.width > window.innerWidth) dialog.style.left = `${window.innerWidth - boxRec.width - DEFAULT_PADDING}px`;
236
+ else dialog.style.left = `${inputRec.left}px`;
237
+ } else {
238
+ // For positioned dialogs, just check if they overflow and adjust if needed
239
+ const currentLeft = parseFloat(dialog.style.left);
240
+ if (!isNaN(currentLeft) && currentLeft + boxRec.width > window.innerWidth) {
241
+ dialog.style.left = `${window.innerWidth - boxRec.width - DEFAULT_PADDING}px`;
242
+ }
243
+ }
244
+
245
+ if (parseFloat(dialog.style.left) < DEFAULT_PADDING) dialog.style.left = `${DEFAULT_PADDING}px`;
246
+
247
+ // ----- Arrow positioning (works for Left / Center / Right) -----
248
+ const dialogRect = dialog.getBoundingClientRect();
249
+ const inputCenterX = inputRec.left + inputRec.width / 2;
250
+
251
+ // Arrow position relative to dialog
252
+ let arrowLeft = inputCenterX - dialogRect.left + ConvertRemToPixels(0.15);
253
+
254
+ // Clamp arrow to dialog bounds (optional but recommended)
255
+ const ARROW_MARGIN = ConvertRemToPixels(1);
256
+ arrowLeft = Math.max(ARROW_MARGIN, Math.min(arrowLeft, boxRec.width - ARROW_MARGIN));
257
+
258
+ dialog.style.setProperty("--arrow-left", `${arrowLeft}px`);
259
+
260
+ const pxWidth = inputRec.width;
261
+ const DEFAULT_WIDTH = ConvertRemToPixels(defaultWidth);
262
+ dialog.style.width = `${Math.max(pxWidth, DEFAULT_WIDTH)}px`;
263
+ };
264
+ const _onMouseEnter = (e: MouseEvent) => {
265
+ if (!triggerElement.matches(":hover")) return;
266
+ if (e.target !== triggerElement) return;
267
+
268
+ const isVisible = triggerElement.getAttribute(VISIBILITY_KEY) === "true";
269
+ if (!isVisible) visibility(true, false);
270
+ };
271
+ let hoverListenersAttached = false;
272
+ let onMouseEnterDebounced: DebouncedFunction<typeof _onMouseEnter> | null = null;
273
+ const getHoverDebounce = () => {
274
+ if (!onMouseEnterDebounced) {
275
+ onMouseEnterDebounced = Debounce(_onMouseEnter, (hoverEffectDelay === true ? DEFAULT_DIALOGBOX_HOVER_EFFECT_DELAY : (hoverEffectDelay as number)) ?? DEFAULT_DIALOGBOX_HOVER_EFFECT_DELAY);
276
+ }
277
+ return onMouseEnterDebounced;
278
+ };
279
+ const onMouseLeaveCancelHover = () => {
280
+ onMouseEnterDebounced?.cancel();
281
+ };
282
+ const attachHoverListeners = () => {
283
+ if (!hoverEffectDelay || hoverListenersAttached) return;
284
+ triggerElement.addEventListener("mouseenter", getHoverDebounce());
285
+ triggerElement.addEventListener("mouseleave", onMouseLeaveCancelHover);
286
+ hoverListenersAttached = true;
287
+ };
288
+ const onHoverProxy = (e: MouseEvent) => {
289
+ attachHoverListeners();
290
+ triggerElement.removeEventListener("mouseenter", onHoverProxy);
291
+ getHoverDebounce()(e);
292
+ };
293
+
294
+ const setupDialog = () => {
295
+ triggerElement.addEventListener("click", onClick);
296
+ if (hoverEffectDelay) triggerElement.addEventListener("mouseenter", onHoverProxy);
297
+ if (isTargetForInput) {
298
+ triggerElement.addEventListener("focusin", onFocus);
299
+ triggerElement.addEventListener("focusout", onFocusOut);
300
+ document.addEventListener("keydown", onKeyDown, true);
301
+ }
302
+
303
+ const timeout = setTimeout(() => {
304
+ clearTimeout(timeout);
305
+ refreshDialog();
306
+ isAutoHideInInitialization = isAutoHideInInitialization ?? true;
307
+ visibility(!isAutoHideInInitialization, true);
308
+ }, 0);
309
+ };
310
+ const cleanupDialog = () => {
311
+ triggerElement.setAttribute(VISIBILITY_KEY, "false");
312
+ triggerElement.removeEventListener("click", onClick);
313
+ if (hoverEffectDelay) {
314
+ triggerElement.removeEventListener("mouseenter", onHoverProxy);
315
+ if (hoverListenersAttached) {
316
+ triggerElement.removeEventListener("mouseenter", getHoverDebounce());
317
+ triggerElement.removeEventListener("mouseleave", onMouseLeaveCancelHover);
318
+ onMouseEnterDebounced?.cancel();
319
+ }
320
+ }
321
+ clearTimeout(visibilityTimeout);
322
+ teardownGlobalDocumentClick();
323
+ if (isTargetForInput) {
324
+ triggerElement.removeEventListener("focusin", onFocus);
325
+ triggerElement.removeEventListener("focusout", onFocusOut);
326
+ document.removeEventListener("keydown", onKeyDown);
327
+ document.removeEventListener("scroll", onDocumentScroll);
328
+ }
329
+ unregisterDialog(triggerElement);
330
+ onDisposed?.();
331
+ };
332
+ let visibilityTimeout: NodeJS.Timeout;
333
+ const visibility = (visible: boolean, isInitial: boolean) => {
334
+ const isVisible = visible;
335
+ if (!triggerElement) return;
336
+ if (isVisible) {
337
+ if (!isInitial) closeOtherDialogs(triggerElement, false);
338
+
339
+ openDialogs.add(triggerElement);
340
+ scheduleGlobalDocumentClick();
341
+
342
+ clearTimeout(visibilityTimeout);
343
+ forceUpdateDebounced.cancel();
344
+ onMouseEnterDebounced?.cancel();
345
+
346
+ onVisibilityChanged?.(isVisible);
347
+ refreshDialog();
348
+ const timeout2 = setTimeout(() => {
349
+ clearTimeout(timeout2);
350
+ refreshDialog();
351
+ }, 0);
352
+
353
+ affectedElement.classList.remove("dn");
354
+ affectedElement.style.pointerEvents = "";
355
+ const timeout = setTimeout(() => {
356
+ clearTimeout(timeout);
357
+ affectedElement.classList.remove("o0");
358
+ affectedElement.classList.add("anim");
359
+ triggerElement.setAttribute(VISIBILITY_KEY, "true");
360
+ }, 10);
361
+ forceUpdateDebounced(0);
362
+
363
+ if (isTargetForInput) document.addEventListener("scroll", onDocumentScroll);
364
+ } else {
365
+ if (!isInitial) closeNestedDialogs(triggerElement, false);
366
+
367
+ openDialogs.delete(triggerElement);
368
+ teardownGlobalDocumentClick();
369
+
370
+ if (isTargetForInput) document.removeEventListener("scroll", onDocumentScroll);
371
+
372
+ affectedElement.classList.add("o0", "anim");
373
+ affectedElement.style.pointerEvents = "none";
374
+ if (isInitial) forceUpdate(1);
375
+ forceUpdateDebounced(1);
376
+ visibilityTimeout = setTimeout(() => {
377
+ if (!isInitial) onVisibilityChanged?.(false);
378
+ clearTimeout(visibilityTimeout);
379
+ triggerElement.setAttribute(VISIBILITY_KEY, "false");
380
+ }, DEFAULT_ANIMATION_DURATION);
381
+ }
382
+ };
383
+ const onKeyDown = (e: KeyboardEvent) => {
384
+ if (IsKeyPressed(e, EKeys.TAB)) if (document.activeElement !== triggerElement) visibility(false, false);
385
+ };
386
+ const onClick = (e: MouseEvent) => {
387
+ const isOpen = triggerElement.getAttribute(VISIBILITY_KEY) === "true";
388
+ const target = e.target as HTMLElement;
389
+ if (isOpen && target.id !== triggerElement.id && target !== triggerElement) return;
390
+
391
+ if (isOpen && isTargetForInput) return;
392
+
393
+ visibility(!isOpen, false);
394
+ e.stopPropagation();
395
+ };
396
+ const onFocus = (e: FocusEvent) => {
397
+ if (isTargetForInput) visibility(true, false);
398
+ };
399
+
400
+ const onFocusOut = (e: FocusEvent) => {
401
+ if (!isTargetForInput) return;
402
+
403
+ if (!triggerElement) return;
404
+ if (!e.relatedTarget || triggerElement.contains(e.relatedTarget as Node | null)) return;
405
+
406
+ visibility(false, false);
407
+ };
408
+ const onDocumentScroll = (e: Event) => {};
409
+
410
+ registerDialog({
411
+ triggerElement,
412
+ affectedElement,
413
+ iggnoreChildClick,
414
+ close: (isInitial) => visibility(false, isInitial),
415
+ });
416
+
417
+ if (!affectedElement.classList.contains("dialog")) {
418
+ affectedElement.className = defaultClass ?? "dialog";
419
+ affectedElement.classList.add("pf", "r", "bs2", "b1", "df", "ddc", `p${defaultPadding}`, background ?? NEXUS_CONFIG.DEFAULT_BACKGROUND, position ?? EPositions.Left);
420
+ setupDialog();
421
+ affectedElement.style.maxWidth = `calc(100vw - ${ConvertRemToPixels(2)}px)`;
422
+ affectedElement.style.maxHeight = `calc(80vh)`;
423
+ }
424
+
425
+ // Add input-dialog class if it's a target for input to handle specific styling
426
+ if (isTargetForInput && !affectedElement.classList.contains("input-dialog")) affectedElement.classList.add("input-dialog");
427
+
428
+ return {
429
+ destroy: cleanupDialog,
430
+ visibility: visibility,
431
+ isValid: () => !!affectedElement?.parentElement,
432
+ } as IDialogboxResults;
433
+ };