typescript-overlay-essentials 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/.github/workflows/npm-publish.yml +32 -0
- package/ConfirmationBox.css +69 -0
- package/ConfirmationBox.tsx +148 -0
- package/Dropdown.css +31 -0
- package/Dropdown.tsx +211 -0
- package/InfoOverlay.css +65 -0
- package/InfoOverlay.tsx +101 -0
- package/InfoOverlayWithInput.css +88 -0
- package/InfoOverlayWithInput.tsx +157 -0
- package/InputFilter.tsx +36 -0
- package/LoadingOverlay.css +110 -0
- package/LoadingOverlay.tsx +57 -0
- package/MultipleChoiceOverlay.css +170 -0
- package/MultipleChoiceOverlay.tsx +170 -0
- package/MultipleRadioOverlay.css +170 -0
- package/MultipleRadioOverlay.tsx +158 -0
- package/README.md +12 -0
- package/Toast.css +13 -0
- package/Toast.tsx +34 -0
- package/ToggleSwitch.tsx +56 -0
- package/Toggleswitch.css +49 -0
- package/index.ts +8 -0
- package/package.json +39 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { useEffect, useState, CSSProperties } from 'react';
|
|
2
|
+
import './MultipleChoiceOverlay.css';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// Um das MultipleChoiceOverlay zu nutzen, muss der State die folgende Struktur haben:
|
|
6
|
+
export interface MultipleChoiceOverlayState {
|
|
7
|
+
headline?: string | undefined;
|
|
8
|
+
message?: string | undefined;
|
|
9
|
+
choices: string[] | [];
|
|
10
|
+
preInput?: string[] | undefined;
|
|
11
|
+
cancelButtonText?: string | undefined;
|
|
12
|
+
proceedButtonText?: string | undefined;
|
|
13
|
+
handlerOk?: ((userInput: string[], args?: unknown) => void) | undefined;
|
|
14
|
+
handlerCancel?: ((args?: unknown) => void) | undefined;
|
|
15
|
+
handlerArgs?: unknown;
|
|
16
|
+
addCloseButton?: boolean | undefined;
|
|
17
|
+
proceedButtonStyle?: CSSProperties | undefined;
|
|
18
|
+
cancelButtonStyle?: CSSProperties | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Der standard MultipleChoiceOverlay Status, der zur Initialisierung genutzt werden kann
|
|
22
|
+
export const defaultMultipleChoiceState: MultipleChoiceOverlayState = {
|
|
23
|
+
headline: undefined,
|
|
24
|
+
message: undefined,
|
|
25
|
+
choices: [],
|
|
26
|
+
preInput: undefined,
|
|
27
|
+
cancelButtonText: undefined,
|
|
28
|
+
proceedButtonText: undefined,
|
|
29
|
+
handlerOk: undefined,
|
|
30
|
+
handlerCancel: undefined,
|
|
31
|
+
handlerArgs: undefined,
|
|
32
|
+
addCloseButton: false,
|
|
33
|
+
proceedButtonStyle: undefined,
|
|
34
|
+
cancelButtonStyle: undefined,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Wobei alle Attribute grundsätzlich optional sind:
|
|
38
|
+
// - headline: ist die Überschrift und wird fett hinterlegt
|
|
39
|
+
// - message: ist die angezeigte Nachricht
|
|
40
|
+
// - choices: Ein String-Array mit den Optionen die ausgewählt werden können (ohne Angabe gibt es keine Auswahl)
|
|
41
|
+
// - preInput: Ein String-Array: Die Einträge sind stadardmäßig ausgewählt alle anderen nicht ausgewählt
|
|
42
|
+
// - cancelButtonText: ist der Text der auf dem Cancel Button stehen soll (ohne Angabe wird "Abbrechen" verwendet)
|
|
43
|
+
// - proceedButtonText: ist der Text der auf dem Proceed Button stehen soll (ohne Angabe wird "OK" verwendet)
|
|
44
|
+
// - handlerOk: ist die Funktion die bei Bestätigung des Inputs ausgeführt wird (Struktur: handlerOk(userInput, handlerArgs)
|
|
45
|
+
// - handlerCancel: ist die Funktion die bei Ablehnung des Inputs ausgeführt wird (Struktur: handlerCancel(handlerArgs)
|
|
46
|
+
// - handlerArgs: kann im handler als Argumente genutzt werden
|
|
47
|
+
// - addCloseButton: boolscher Wert, der angibt, ob ein x oben rechts als close-Button verfügbar sein soll (bricht die Aktion ohne handler ab, standardmäßig false)
|
|
48
|
+
// - proceedButtonStyle: ist der Style des Bestätigungsbuttons (Standardmäßig unverändert)
|
|
49
|
+
// - cancelButtonStyle: ist der Style des Abbrechenbuttons (Standardmäßig unverändert)
|
|
50
|
+
|
|
51
|
+
// Output: Der Input vom User wird am Ende an den handlerOk übergeben (oder bei handlerCancel ignoriert)!
|
|
52
|
+
// Somit wird der handler so aufgerufen: handlerOk(UserInput, handlerArgs) oder handlerCancel(handlerArgs)
|
|
53
|
+
export function MultipleChoiceOverlay({ state, setState }: { state: MultipleChoiceOverlayState, setState: (state: MultipleChoiceOverlayState) => void }): React.ReactElement {
|
|
54
|
+
|
|
55
|
+
// Wird verwendet um das Infoverlay ein- und auszublenden
|
|
56
|
+
const [showOverlay, setShowOverlay] = useState(false);
|
|
57
|
+
|
|
58
|
+
// Wird verwendet um die ausgewählten Choices aktuell zu halten
|
|
59
|
+
const [input, setInput] = useState<string[]>([]);
|
|
60
|
+
|
|
61
|
+
// Sobald der State von außen aktualisiert wird triggert diese Funktion
|
|
62
|
+
// Die setzt showOverlay auf true
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (state?.message != null || state?.headline != null) {
|
|
65
|
+
setInput(state?.preInput != null ? state.preInput : []);
|
|
66
|
+
setShowOverlay(true);
|
|
67
|
+
setTimeout(() => { document.getElementById("confirmButton")?.focus(); }, 1);
|
|
68
|
+
}
|
|
69
|
+
}, [state]);
|
|
70
|
+
|
|
71
|
+
// Toggled die Auswahl eines Choices
|
|
72
|
+
// Setzt dafür den aktuellen Input auf den neuen Input + Choice
|
|
73
|
+
const toggle = (choice : string) : void => {
|
|
74
|
+
if (input.includes(choice)) {
|
|
75
|
+
setInput(input.filter(item => item !== choice)); // Entfernt den Choice wenn er schon ausgewählt ist
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
setInput([...input, choice]); // Fügt den Choice hinzu wenn er noch nicht ausgewählt ist
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const handleAction = (
|
|
83
|
+
handler: ((args?: unknown) => void) | ((userInput: string[], args?: unknown) => void) | undefined,
|
|
84
|
+
useInput: boolean
|
|
85
|
+
): void => {
|
|
86
|
+
setShowOverlay(false);
|
|
87
|
+
var tempState = {
|
|
88
|
+
handler: handler,
|
|
89
|
+
handlerArgs: state.handlerArgs
|
|
90
|
+
};
|
|
91
|
+
setState(defaultMultipleChoiceState);
|
|
92
|
+
if (typeof tempState.handler === 'function' && useInput)
|
|
93
|
+
(tempState.handler as ((userInput: string[], args?: unknown) => void))(input, tempState.handlerArgs);
|
|
94
|
+
else if (typeof tempState.handler === 'function')
|
|
95
|
+
(tempState.handler as ((args?: unknown) => void))(tempState.handlerArgs);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return showOverlay ?
|
|
99
|
+
<div className="multiplechoice-overlay">
|
|
100
|
+
<div className="multiplechoice-box">
|
|
101
|
+
{state.addCloseButton?
|
|
102
|
+
<span className="closeButton">
|
|
103
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
104
|
+
tabIndex={0}
|
|
105
|
+
className="close-icon"
|
|
106
|
+
onClick={() => handleAction(undefined, false)}
|
|
107
|
+
onKeyDown={(e) => {
|
|
108
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
109
|
+
e.preventDefault(); // Verhindert Scroll bei Space
|
|
110
|
+
handleAction(undefined, false);
|
|
111
|
+
}
|
|
112
|
+
}}
|
|
113
|
+
role="button" aria-label="Dialog schließen"
|
|
114
|
+
width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
115
|
+
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
116
|
+
<line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>
|
|
117
|
+
</svg>
|
|
118
|
+
</span> :
|
|
119
|
+
<></>}
|
|
120
|
+
<p className="headline" id="theHeadline" tabIndex={0} style={{ whiteSpace: "pre-line", wordBreak: "break-word" }}><strong>{state?.headline != null ? state.headline : ""}</strong></p>
|
|
121
|
+
<p tabIndex={0} style={{ whiteSpace: "pre-line", wordBreak: "break-word" }}>{state?.message != null ? state.message : ""}</p>
|
|
122
|
+
<div className="choices-input">
|
|
123
|
+
<div className="choices-container">
|
|
124
|
+
{state?.choices.map(choice => (
|
|
125
|
+
<label className="choice-label" key={choice}>
|
|
126
|
+
<input
|
|
127
|
+
type="checkbox"
|
|
128
|
+
checked={input.includes(choice)}
|
|
129
|
+
onChange={() => toggle(choice)}
|
|
130
|
+
tabIndex={0}
|
|
131
|
+
onKeyDown={(e) => {
|
|
132
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
133
|
+
e.preventDefault(); // Verhindert Scroll bei Space
|
|
134
|
+
toggle(choice);
|
|
135
|
+
}
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
{choice}
|
|
139
|
+
</label>
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
<div className="information-buttons">
|
|
144
|
+
<button onClick={() => handleAction(state.handlerCancel, false)}
|
|
145
|
+
onKeyDown={(event) => {
|
|
146
|
+
if (event.key === "Enter" || event.key === ' ') {
|
|
147
|
+
event.preventDefault(); // Verhindert Scroll bei Space
|
|
148
|
+
handleAction(state.handlerCancel, false);
|
|
149
|
+
}
|
|
150
|
+
}}
|
|
151
|
+
className="px-4 py-2 bg-gray-300 rounded"
|
|
152
|
+
style={state?.cancelButtonStyle != null ? state.cancelButtonStyle : {}}>
|
|
153
|
+
{state?.cancelButtonText != null ? state.cancelButtonText : "Abbrechen"}
|
|
154
|
+
</button>
|
|
155
|
+
<button onClick={() => handleAction(state.handlerOk, true)}
|
|
156
|
+
id="confirmButton"
|
|
157
|
+
onKeyDown={(event) => {
|
|
158
|
+
if (event.key === "Enter" || event.key === ' ') {
|
|
159
|
+
event.preventDefault(); // Verhindert Scroll bei Space
|
|
160
|
+
handleAction(state.handlerOk, true);
|
|
161
|
+
}
|
|
162
|
+
}}
|
|
163
|
+
className="px-4 py-2 bg-blue-600 text-white rounded"
|
|
164
|
+
style={state?.proceedButtonStyle != null ? state.proceedButtonStyle : {}}>
|
|
165
|
+
{state?.proceedButtonText != null ? state.proceedButtonText : "OK"}
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div> : <></>;
|
|
170
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
.multipleradio-overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
width: 100vw;
|
|
6
|
+
height: 100vh;
|
|
7
|
+
background-color: rgba(0, 0, 0, 0.6);
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
z-index: 10000;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.multipleradio-box {
|
|
15
|
+
background-color: var(--Hellgrau, white);
|
|
16
|
+
color: var(--HellSchwarz,#2c2c2c);
|
|
17
|
+
padding: 2rem;
|
|
18
|
+
border-radius: 12px;
|
|
19
|
+
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5);
|
|
20
|
+
max-width: 500px;
|
|
21
|
+
width: 90%;
|
|
22
|
+
text-align: center;
|
|
23
|
+
animation: fadeIn 0.3s ease-in-out;
|
|
24
|
+
white-space: pre-line;
|
|
25
|
+
position: relative;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@keyframes fadeIn {
|
|
29
|
+
from {
|
|
30
|
+
opacity: 0;
|
|
31
|
+
transform: scale(0.95);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
to {
|
|
35
|
+
opacity: 1;
|
|
36
|
+
transform: scale(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.multipleradio-box .headline {
|
|
41
|
+
margin-bottom: 0.75em;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.multipleradio-box .information-buttons {
|
|
45
|
+
display: flex;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
gap: 1rem;
|
|
48
|
+
margin-top: 1.5rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.multipleradio-box .information-buttons button {
|
|
52
|
+
font-size: 85%;
|
|
53
|
+
padding: 0.5rem 1.1rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* === Stylisierte Checkbox === */
|
|
57
|
+
.multipleradio-box .radios-input {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
align-items: center;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.multipleradio-box .radios-input input[type="checkbox"] {
|
|
64
|
+
appearance: none;
|
|
65
|
+
width: 20px;
|
|
66
|
+
height: 20px;
|
|
67
|
+
border: 2px solid white;
|
|
68
|
+
background-color: var(--FastWeiß, white);
|
|
69
|
+
border-color: var(--Mittelgrau, #ccc);
|
|
70
|
+
border-radius: 4px;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
transition: background-color 0.2s, border-color 0.2s;
|
|
73
|
+
position: relative;
|
|
74
|
+
margin-right: 0.5rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.multipleradio-box .radios-input input[type="checkbox"]:checked {
|
|
78
|
+
background-color: var(--DunkelAkzent, #4aabf5) ;
|
|
79
|
+
border-color: var(--DunkelAkzent, #4aabf5) ;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.multipleradio-box .radios-input input[type="checkbox"]:hover {
|
|
83
|
+
background-color: var(--Hellgrau, #ddd);
|
|
84
|
+
border-color: var(--Dunkelgrau, #999);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.multipleradio-box .radios-input input[type="checkbox"]:checked:hover {
|
|
88
|
+
background-color: var(--HSDAkzent, #4aabf5) ;
|
|
89
|
+
border-color: var(--HSDAkzent, #4aabf5) ;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
/* Häkchen aus zwei Linien */
|
|
94
|
+
.multipleradio-box .radios-input input[type="checkbox"]:checked::after {
|
|
95
|
+
content: "";
|
|
96
|
+
position: absolute;
|
|
97
|
+
left: 5px;
|
|
98
|
+
top: 2px;
|
|
99
|
+
width: 5px;
|
|
100
|
+
height: 10px;
|
|
101
|
+
border: solid white;
|
|
102
|
+
border-width: 0 2px 2px 0;
|
|
103
|
+
transform: rotate(45deg);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.multipleradio-box .radios-input input[type="checkbox"]:focus {
|
|
107
|
+
outline: none;
|
|
108
|
+
box-shadow: 0 0 0 2px var(--MittelAkzent, rgba(74, 171, 245));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.multipleradio-box .radio-label {
|
|
112
|
+
justify-content: flex-start;
|
|
113
|
+
display: flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
margin-bottom: 0.5rem;
|
|
116
|
+
vertical-align: middle;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.multipleradio-box .radios-container{
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-direction: column;
|
|
122
|
+
align-items: flex-start;
|
|
123
|
+
gap: 0.5rem;
|
|
124
|
+
width: max-content;
|
|
125
|
+
padding: 1rem;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Light Mode */
|
|
129
|
+
@media (prefers-color-scheme: dark) {
|
|
130
|
+
|
|
131
|
+
.multipleradio-box {
|
|
132
|
+
background-color: var(--HellSchwarz,#2c2c2c);
|
|
133
|
+
color: var(--FastWeiß, white);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.multipleradio-box .radios-input input[type="checkbox"] {
|
|
137
|
+
background-color: var(--Dunkelgrau, #888);
|
|
138
|
+
background-color: var(--FastSchwarz,#222);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.multipleradio-box .radios-input input[type="checkbox"]:checked {
|
|
142
|
+
background-color: var(--DunkelAkzent, #4aabf5);
|
|
143
|
+
border-color: var(--DunkelAkzent, #4aabf5);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.multipleradio-box .radios-input input[type="checkbox"]:hover {
|
|
147
|
+
background-color: var(--Dunkelgrau, #ddd);
|
|
148
|
+
border-color: var(--Hellgrau, #999);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.multipleradio-box .radios-input input[type="checkbox"]:checked:hover {
|
|
152
|
+
background-color: var(--HSDAkzent, #4aabf5) ;
|
|
153
|
+
border-color: var(--HSDAkzent, #4aabf5) ;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.multipleradio-box .radios-input input[type="checkbox"]:checked::after {
|
|
157
|
+
border-color: white;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.multipleradio-box .closeButton {
|
|
162
|
+
right: 10px;
|
|
163
|
+
top: 10px;
|
|
164
|
+
position: absolute;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.multipleradio-box .close-icon:hover {
|
|
168
|
+
color: var(--HSDAkzent);
|
|
169
|
+
cursor: pointer;
|
|
170
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { useEffect, useState, CSSProperties } from 'react';
|
|
2
|
+
import './MultipleRadioOverlay.css';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// Um das MultipleRadioOverlay zu nutzen, muss der State die folgende Struktur haben:
|
|
6
|
+
export interface MultipleRadioOverlayState {
|
|
7
|
+
headline?: string | undefined;
|
|
8
|
+
message?: string | undefined;
|
|
9
|
+
choices: string[] | [];
|
|
10
|
+
preInput?: string | undefined;
|
|
11
|
+
cancelButtonText?: string | undefined;
|
|
12
|
+
proceedButtonText?: string | undefined;
|
|
13
|
+
handlerOk?: ((userInput: string, args?: unknown) => void) | undefined;
|
|
14
|
+
handlerCancel?: ((args?: unknown) => void) | undefined;
|
|
15
|
+
handlerArgs?: unknown;
|
|
16
|
+
addCloseButton?: boolean | undefined;
|
|
17
|
+
proceedButtonStyle?: CSSProperties | undefined;
|
|
18
|
+
cancelButtonStyle?: CSSProperties | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Um das MultipleRadioOverlay zu nutzen, muss der State die folgende Struktur haben:
|
|
22
|
+
export const defaultMultipleRadioState: MultipleRadioOverlayState = {
|
|
23
|
+
headline: undefined,
|
|
24
|
+
message: undefined,
|
|
25
|
+
choices: [],
|
|
26
|
+
preInput: undefined,
|
|
27
|
+
cancelButtonText: undefined,
|
|
28
|
+
proceedButtonText: undefined,
|
|
29
|
+
handlerOk: undefined,
|
|
30
|
+
handlerCancel: undefined,
|
|
31
|
+
handlerArgs: undefined,
|
|
32
|
+
addCloseButton: false,
|
|
33
|
+
proceedButtonStyle: undefined,
|
|
34
|
+
cancelButtonStyle: undefined,
|
|
35
|
+
};
|
|
36
|
+
// Wobei alle Attribute grundsätzlich optional sind:
|
|
37
|
+
// - headline: ist die Überschrift und wird fett hinterlegt
|
|
38
|
+
// - message: ist die angezeigte Nachricht
|
|
39
|
+
// - choices: Ein String-Array mit den Optionen die ausgewählt werden können (ohne Angabe gibt es keine Auswahl)
|
|
40
|
+
// - preInput: Ein String: Dieser Eintrag ist stadardmäßig ausgewählt
|
|
41
|
+
// - cancelButtonText: ist der Text der auf dem Cancel Button stehen soll (ohne Angabe wird "Abbrechen" verwendet)
|
|
42
|
+
// - proceedButtonText: ist der Text der auf dem Proceed Button stehen soll (ohne Angabe wird "OK" verwendet)
|
|
43
|
+
// - handlerOk: ist die Funktion die bei Bestätigung des Inputs ausgeführt wird (Struktur: handlerOk(userInput, handlerArgs)
|
|
44
|
+
// - handlerCancel: ist die Funktion die bei Ablehnung des Inputs ausgeführt wird (Struktur: handlerCancel(handlerArgs)
|
|
45
|
+
// - handlerArgs: kann im handler als Argumente genutzt werden
|
|
46
|
+
// - addCloseButton: boolscher Wert, der angibt, ob ein x oben rechts als close-Button verfügbar sein soll (bricht die Aktion ohne handler ab, standardmäßig false)
|
|
47
|
+
// - proceedButtonStyle: ist der Style des Bestätigungsbuttons (Standardmäßig unverändert)
|
|
48
|
+
// - cancelButtonStyle: ist der Style des Abbrechenbuttons (Standardmäßig unverändert)
|
|
49
|
+
|
|
50
|
+
// Output: Der Input vom User wird am Ende an den handlerOk übergeben (oder bei handlerCancel ignoriert)!
|
|
51
|
+
// Somit wird der handler so aufgerufen: handlerOk(UserInput, handlerArgs) oder handlerCancel(handlerArgs)
|
|
52
|
+
export function MultipleRadioOverlay({ state, setState }: { state: MultipleRadioOverlayState, setState: (state: MultipleRadioOverlayState) => void }): React.ReactElement {
|
|
53
|
+
|
|
54
|
+
// Wird verwendet um das Infoverlay ein- und auszublenden
|
|
55
|
+
const [showOverlay, setShowOverlay] = useState<boolean>(false);
|
|
56
|
+
|
|
57
|
+
// Wird verwendet um die ausgewählte Wahl aktuell zu halten
|
|
58
|
+
const [input, setInput] = useState<string>("");
|
|
59
|
+
|
|
60
|
+
// Sobald der State von außen aktualisiert wird triggert diese Funktion
|
|
61
|
+
// Die setzt showOverlay auf true
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (state?.message != null || state?.headline != null) {
|
|
64
|
+
setInput(state?.preInput != null ? state.preInput : "");
|
|
65
|
+
setShowOverlay(true);
|
|
66
|
+
setTimeout(() => { document.getElementById("confirmButton")?.focus(); }, 1);
|
|
67
|
+
}
|
|
68
|
+
}, [state]);
|
|
69
|
+
|
|
70
|
+
const handleAction = (
|
|
71
|
+
handler: ((args?: unknown) => void) | ((userInput: string, args?: unknown) => void) | undefined,
|
|
72
|
+
useInput: boolean
|
|
73
|
+
): void => {
|
|
74
|
+
setShowOverlay(false);
|
|
75
|
+
var tempState = {
|
|
76
|
+
handler: handler,
|
|
77
|
+
handlerArgs: state.handlerArgs
|
|
78
|
+
};
|
|
79
|
+
setState(defaultMultipleRadioState);
|
|
80
|
+
if (typeof tempState.handler === 'function' && useInput)
|
|
81
|
+
(tempState.handler as ((userInput: string, args?: unknown) => void))(input, tempState.handlerArgs);
|
|
82
|
+
else if (typeof tempState.handler === 'function')
|
|
83
|
+
(tempState.handler as ((args?: unknown) => void))(tempState.handlerArgs);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return showOverlay ?
|
|
87
|
+
<div className="multipleradio-overlay">
|
|
88
|
+
<div className="multipleradio-box">
|
|
89
|
+
{state.addCloseButton?
|
|
90
|
+
<span className="closeButton">
|
|
91
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
92
|
+
tabIndex={0}
|
|
93
|
+
className="close-icon"
|
|
94
|
+
onClick={() => handleAction(undefined, false)}
|
|
95
|
+
onKeyDown={(e) => {
|
|
96
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
97
|
+
e.preventDefault(); // Verhindert Scroll bei Space
|
|
98
|
+
handleAction(undefined, false);
|
|
99
|
+
}
|
|
100
|
+
}}
|
|
101
|
+
role="button" aria-label="Dialog schließen"
|
|
102
|
+
width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
103
|
+
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
104
|
+
<line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>
|
|
105
|
+
</svg>
|
|
106
|
+
</span> :
|
|
107
|
+
<></>}
|
|
108
|
+
<p className="headline" id = "theHeadline" tabIndex={0} style={{ whiteSpace: "pre-line", wordBreak: "break-word" }}><strong>{state?.headline != null ? state.headline : ""}</strong></p>
|
|
109
|
+
<p tabIndex={0} style={{ whiteSpace: "pre-line", wordBreak: "break-word" }}>{state?.message != null ? state.message : ""}</p>
|
|
110
|
+
<div className="radios-input">
|
|
111
|
+
<div className="radios-container">
|
|
112
|
+
{state?.choices.map(choice => (
|
|
113
|
+
<label className="radio-label" key={choice}>
|
|
114
|
+
<input
|
|
115
|
+
type="radio"
|
|
116
|
+
checked={input === choice}
|
|
117
|
+
onChange={() => setInput(choice)}
|
|
118
|
+
tabIndex={0}
|
|
119
|
+
onKeyDown={(e) => {
|
|
120
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
121
|
+
e.preventDefault(); // Verhindert Scroll bei Space
|
|
122
|
+
setInput(choice);
|
|
123
|
+
}
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
{choice}
|
|
127
|
+
</label>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
<div className="information-buttons">
|
|
132
|
+
<button onClick={() => handleAction(state.handlerCancel, false)}
|
|
133
|
+
onKeyDown={(event) => {
|
|
134
|
+
if (event.key === "Enter" || event.key === ' ') {
|
|
135
|
+
event.preventDefault(); // Verhindert Scroll bei Space
|
|
136
|
+
handleAction(state.handlerCancel, false);
|
|
137
|
+
}
|
|
138
|
+
}}
|
|
139
|
+
className="px-4 py-2 bg-gray-300 rounded"
|
|
140
|
+
style={state?.cancelButtonStyle != null ? state.cancelButtonStyle : {}}>
|
|
141
|
+
{state?.cancelButtonText != null ? state.cancelButtonText : "Abbrechen"}
|
|
142
|
+
</button>
|
|
143
|
+
<button onClick={() => handleAction(state.handlerOk, true)}
|
|
144
|
+
id="confirmButton"
|
|
145
|
+
onKeyDown={(event) => {
|
|
146
|
+
if (event.key === "Enter" || event.key === ' ') {
|
|
147
|
+
event.preventDefault(); // Verhindert Scroll bei Space
|
|
148
|
+
handleAction(state.handlerOk, true);
|
|
149
|
+
}
|
|
150
|
+
}}
|
|
151
|
+
className="px-4 py-2 bg-blue-600 text-white rounded"
|
|
152
|
+
style={state?.proceedButtonStyle != null ? state.proceedButtonStyle : {}}>
|
|
153
|
+
{state?.proceedButtonText != null ? state.proceedButtonText : "OK"}
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div> : <></>;
|
|
158
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# typescript-overlay-essentials
|
|
2
|
+
Eine kleine Ansammlung an praktischen Tools, die out of the Box genutzt werden können. Alle enthaltenen Overlays können als Reaktion auf etwas geöffnet werden und schließen sich danach automatisch. Es beinhaltet:
|
|
3
|
+
- [ConfirmationBox]: Ein Overlay mit einer ConfirmationBox (Fortfahren / Abbrechen),
|
|
4
|
+
- [InfoOverlay]: ein simples Overlay mit einer Informationsbox die man bestätigen kann,
|
|
5
|
+
- [InfoOverlayWithInput]: Ein Overlay bei dem ein User zusätzlich ein Freitext-Feld zur Eingabe hat,
|
|
6
|
+
- [LoadingOverlay]: Ein simples Overlay mit einer Lade-animation und optionalem Ladetext,
|
|
7
|
+
- [MultipleChoiceOverlay]: Ein simples Overlay bei dem ein Nutzer aus mehreren gegebenen Aktionen auswählen kann,
|
|
8
|
+
- [MultipleRadioOverlay]: Ein simples Overlay bei dem ein Nutzer genau eine aus mehreren gegebenen Aktionen auswählen kann,
|
|
9
|
+
- [Toast]: Ein simpler Toast der für kurze Zeit grün hinterlegt in der oberen rechten Ecke des Bildschirms angezeigt wird,
|
|
10
|
+
- [ToggleSwitch]: Eine Auswahl aus zwei Optionen aus denen ein Nutzer wählen kann in der Darstellung (Option 1 | Option 2),
|
|
11
|
+
- [Dropdown]: Ein simples Dropdown-Menü (auf Basis von react-select), welches einfacherer zu Konfigurieren ist und einige default-Einstellungen hat,
|
|
12
|
+
- [InputFilter]: Ein Tool was genutzt werden kann um auf einem Inputfeld einen Regex-Filter anzuwenden und dem User so nur Eingaben erlaubt die dem Filter entsprechen.
|
package/Toast.css
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
.toast-notification {
|
|
2
|
+
font-size: 80%;
|
|
3
|
+
position: fixed;
|
|
4
|
+
top: 14rem;
|
|
5
|
+
right: 1rem;
|
|
6
|
+
background-color: var(--DunkleresGruen, #1a7f37);
|
|
7
|
+
color: white;
|
|
8
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
|
9
|
+
padding: 0.8rem 1.2rem 0.65rem 1.2rem;
|
|
10
|
+
border-radius: 8px;
|
|
11
|
+
animation: fadeInOut 2.5s ease-in-out;
|
|
12
|
+
z-index: 1000;
|
|
13
|
+
}
|
package/Toast.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import './Toast.css'
|
|
4
|
+
|
|
5
|
+
// Zeigt einen kleinen Toast in der oberen rechten Ecke an, der nach 2,5 Sekunden wieder verschwindet
|
|
6
|
+
function Toast({ state, setState }: { state: string, setState: (value: string) => void }) : React.ReactElement {
|
|
7
|
+
// Wird verwendet um die Toast-Notification ein-/auszublenden
|
|
8
|
+
const [showToast, setShowToast] = useState(false);
|
|
9
|
+
// Wird verwendet um den Inhalt der Toast-Notification zu bestimmen
|
|
10
|
+
const [toastContent, setToastContent] = useState("");
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
setToastContent(state)
|
|
14
|
+
}, [state]);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (toastContent && toastContent !== "") {
|
|
18
|
+
setShowToast(true);
|
|
19
|
+
setTimeout(() => { setState(""); }, 2500); // Hinweis nach 2,5 Sekunden wieder ausblenden
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
setShowToast(false);
|
|
23
|
+
}
|
|
24
|
+
}, [toastContent, setState]);
|
|
25
|
+
|
|
26
|
+
return <>
|
|
27
|
+
{showToast && (
|
|
28
|
+
<div className="toast-notification">
|
|
29
|
+
{toastContent}
|
|
30
|
+
</div>)}
|
|
31
|
+
</>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default Toast;
|
package/ToggleSwitch.tsx
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { useRef, useEffect, useState } from "react";
|
|
2
|
+
import "./ToggleSwitch.css";
|
|
3
|
+
|
|
4
|
+
export interface Option<T> {
|
|
5
|
+
value: T;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Attribute vom ToggleSwitch:
|
|
10
|
+
// - optionLeft: {value: valueLeft, label: labelLeft} Beschreibt die linke Option. valueLeft ist der Wert, der in "value" eingetragen wird und labelLeft, was als Option angezeigt wird.
|
|
11
|
+
// - optionRight: {value: valueLeft, label: labelRight} Beschreibt die rechte Option. valueRight ist der Wert, der in "value" eingetragen wird und labelRight, was als Option angezeigt wird.
|
|
12
|
+
// - value: Der Wert, der ausgewählt angezeigt werden soll (useState)
|
|
13
|
+
// - onChange: Die Funktion die bei Änderung ausgeführt werden soll.
|
|
14
|
+
// Standardmäßig könnte z.B. {(value) => {value === valueLeft ? setValue(valueLeft) : setValue(valueRight)}} genutzt werden
|
|
15
|
+
|
|
16
|
+
export function ToggleSwitch<T>({ optionLeft, optionRight, value, onChange } : { optionLeft: Option<T>, optionRight: Option<T>, value: T, onChange: (value: T) => void }) {
|
|
17
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
const optionLeftRef = useRef<HTMLDivElement>(null);
|
|
19
|
+
const optionRightRef = useRef<HTMLDivElement>(null);
|
|
20
|
+
const [sliderStyle, setSliderStyle] = useState({});
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const el = value === optionLeft.value ? optionLeftRef.current : optionRightRef.current;
|
|
24
|
+
const container = containerRef.current;
|
|
25
|
+
|
|
26
|
+
if (!el || !container) return;
|
|
27
|
+
|
|
28
|
+
const { offsetLeft, offsetWidth } = el;
|
|
29
|
+
|
|
30
|
+
setSliderStyle({
|
|
31
|
+
transform: `translateX(calc(${offsetLeft}px - 1rem))`,
|
|
32
|
+
width: `calc(${offsetWidth}px + 2rem)`
|
|
33
|
+
});
|
|
34
|
+
}, [value, optionLeft, optionRight]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="toggleWrapper" ref={containerRef}>
|
|
38
|
+
<div
|
|
39
|
+
className={`toggleOption ${value === optionLeft.value ? "active" : ""}`}
|
|
40
|
+
ref={el => {optionLeftRef.current = el}}
|
|
41
|
+
onClick={() => onChange(optionLeft.value)}
|
|
42
|
+
>
|
|
43
|
+
{optionLeft.label}
|
|
44
|
+
</div>
|
|
45
|
+
<div
|
|
46
|
+
className={`toggleOption ${value === optionRight.value ? "active" : ""}`}
|
|
47
|
+
ref={el => {optionRightRef.current = el}}
|
|
48
|
+
onClick={() => onChange(optionRight.value)}
|
|
49
|
+
>
|
|
50
|
+
{optionRight.label}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className={`toggleSlider ${value === optionLeft.value ? "left" : "right"}`} style={sliderStyle}/>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
package/Toggleswitch.css
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.toggleWrapper {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: flex;
|
|
4
|
+
width: fit-content;
|
|
5
|
+
height: 40px;
|
|
6
|
+
border-radius: 20px;
|
|
7
|
+
background-color: #f9eeee;;
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
cursor: pointer;
|
|
10
|
+
user-select: none;
|
|
11
|
+
font-family: sans-serif;
|
|
12
|
+
font-weight: 500;
|
|
13
|
+
float: right;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.toggleOption {
|
|
17
|
+
flex: 1;
|
|
18
|
+
display: flex;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
align-items: center;
|
|
21
|
+
z-index: 2;
|
|
22
|
+
color: var(--FastSchwarz, black);
|
|
23
|
+
transition: color 0.2s;
|
|
24
|
+
margin: 1rem;
|
|
25
|
+
white-space: nowrap;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.toggleOption.active {
|
|
29
|
+
color: black;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.toggleSlider {
|
|
33
|
+
position: absolute;
|
|
34
|
+
top: 0;
|
|
35
|
+
bottom: 0;
|
|
36
|
+
width: 50%;
|
|
37
|
+
border-radius: 20px;
|
|
38
|
+
background-color: var(--HellAkzent, red);
|
|
39
|
+
transition: transform 0.25s ease;
|
|
40
|
+
z-index: 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.toggleSlider.left {
|
|
44
|
+
transform: translateX(0%);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.toggleSlider.right {
|
|
48
|
+
transform: translateX(100%);
|
|
49
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ConfirmationBox, ConfirmationBoxState, defaultConfirmationState } from './ConfirmationBox.tsx';
|
|
2
|
+
export { Dropdown, Option } from './Dropdown.tsx';
|
|
3
|
+
export { InfoOverlay, InfoOverlayState, defaultInfoOverlayState } from './InfoOverlay.tsx';
|
|
4
|
+
export { InfoOverlayWithInput, InfoOverlayWithInputState, defaultInformationState } from './InfoOverlayWithInput.tsx';
|
|
5
|
+
export { setInputFilter } from './InputFilter.tsx';
|
|
6
|
+
export { LoadingOverlay, LoadingOverlayState, defaultLoadingOverlayState } from './LoadingOverlay.tsx';
|
|
7
|
+
export { MultipleChoiceOverlay, MultipleChoiceOverlayState, defaultMultipleChoiceState } from './MultipleChoiceOverlay.tsx';
|
|
8
|
+
export { MultipleRadioOverlay, MultipleRadioOverlayState, defaultMultipleRadioState } from './MultipleRadioOverlay.tsx';
|