@enigmax/primitives 0.20.0 → 0.22.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.
@@ -0,0 +1,207 @@
1
+ import React from "react";
2
+ import type { ExternalToast, PromiseData, PromiseT, ToastT, ToastToDismiss, ToastTypes } from "./types";
3
+
4
+ let toastsCounter = 1;
5
+
6
+ type titleT = (() => React.ReactNode) | React.ReactNode;
7
+
8
+ class Observer {
9
+ subscribers: Array<(toast: ExternalToast | ToastToDismiss) => void>;
10
+ toasts: Array<ToastT | ToastToDismiss>;
11
+
12
+ constructor() {
13
+ this.subscribers = [];
14
+ this.toasts = [];
15
+ }
16
+
17
+ // We use arrow functions to maintain the correct `this` reference.
18
+ subscribe = (subscriber: (toast: ToastT | ToastToDismiss | ExternalToast) => void) => {
19
+ this.subscribers.push(subscriber);
20
+ return () => {
21
+ const index = this.subscribers.indexOf(subscriber);
22
+ this.subscribers.splice(index, 1);
23
+ };
24
+ };
25
+
26
+ publish = (data: ToastT) => {
27
+ this.subscribers.forEach((subscriber) => subscriber(data));
28
+ };
29
+
30
+ addToast = (data: ToastT) => {
31
+ this.publish(data);
32
+ this.toasts = [...this.toasts, data];
33
+ };
34
+
35
+ create = (
36
+ data: ExternalToast & {
37
+ message?: titleT;
38
+ type?: ToastTypes;
39
+ promise?: PromiseT;
40
+ jsx?: React.ReactElement;
41
+ }
42
+ ) => {
43
+ const { message, ...rest } = data;
44
+ const id = typeof data?.id === "number" || (typeof data?.id === "string" && data.id?.length > 0) ? data.id : toastsCounter++;
45
+ const alreadyExists = this.toasts.find((toast) => toast.id === id);
46
+ const dismissible = data.dismissible === undefined ? true : data.dismissible;
47
+ if (alreadyExists) {
48
+ this.toasts = this.toasts.map((toast) => {
49
+ if (toast.id === id) {
50
+ this.publish({ ...toast, ...data, id, title: message });
51
+ return {
52
+ ...toast,
53
+ ...data,
54
+ id,
55
+ dismissible,
56
+ title: message
57
+ };
58
+ }
59
+ return toast;
60
+ });
61
+ } else this.addToast({ title: message, ...rest, dismissible, id });
62
+ return id;
63
+ };
64
+
65
+ dismiss = (id?: number | string) => {
66
+ if (!id) this.toasts.forEach((toast) => this.subscribers.forEach((subscriber) => subscriber({ id: toast.id, dismiss: true })));
67
+ this.subscribers.forEach((subscriber) => subscriber({ id, dismiss: true }));
68
+ return id;
69
+ };
70
+
71
+ message = (message: titleT | React.ReactNode, data?: ExternalToast) => {
72
+ return this.create({ ...data, message });
73
+ };
74
+
75
+ error = (message: titleT | React.ReactNode, data?: ExternalToast) => {
76
+ return this.create({ ...data, message, type: "error" });
77
+ };
78
+
79
+ success = (message: titleT | React.ReactNode, data?: ExternalToast) => {
80
+ return this.create({ ...data, type: "success", message });
81
+ };
82
+
83
+ info = (message: titleT | React.ReactNode, data?: ExternalToast) => {
84
+ return this.create({ ...data, type: "info", message });
85
+ };
86
+
87
+ warning = (message: titleT | React.ReactNode, data?: ExternalToast) => {
88
+ return this.create({ ...data, type: "warning", message });
89
+ };
90
+
91
+ loading = (message: titleT | React.ReactNode, data?: ExternalToast) => {
92
+ return this.create({ ...data, type: "loading", message });
93
+ };
94
+
95
+ promise = <ToastData>(promise: PromiseT<ToastData>, data?: PromiseData<ToastData>) => {
96
+ if (!data) return;
97
+
98
+ let id: string | number | undefined = undefined;
99
+ if (data.loading !== undefined) {
100
+ id = this.create({
101
+ ...data,
102
+ promise,
103
+ type: "loading",
104
+ message: data.loading,
105
+ description: typeof data.description !== "function" ? data.description : undefined
106
+ });
107
+ }
108
+
109
+ const p = promise instanceof Promise ? promise : promise();
110
+
111
+ let shouldDismiss = id !== undefined;
112
+ let result: ["resolve", ToastData] | ["reject", unknown];
113
+
114
+ const originalPromise = p
115
+ .then(async (response) => {
116
+ result = ["resolve", response];
117
+ const isReactElementResponse = React.isValidElement(response);
118
+ if (isReactElementResponse) {
119
+ shouldDismiss = false;
120
+ this.create({ id, type: "default", message: response });
121
+ } else if (isHttpResponse(response) && !response.ok) {
122
+ shouldDismiss = false;
123
+ const message = typeof data.error === "function" ? await data.error(`HTTP error! status: ${response.status}`) : data.error;
124
+ const description = typeof data.description === "function" ? await data.description(`HTTP error! status: ${response.status}`) : data.description;
125
+ this.create({ id, type: "error", message, description });
126
+ } else if (data.success !== undefined) {
127
+ shouldDismiss = false;
128
+ const message = typeof data.success === "function" ? await data.success(response) : data.success;
129
+ const description = typeof data.description === "function" ? await data.description(response) : data.description;
130
+ this.create({ id, type: "success", message, description });
131
+ }
132
+ })
133
+ .catch(async (error) => {
134
+ result = ["reject", error];
135
+ if (data.error !== undefined) {
136
+ shouldDismiss = false;
137
+ const message = typeof data.error === "function" ? await data.error(error) : data.error;
138
+ const description = typeof data.description === "function" ? await data.description(error) : data.description;
139
+ this.create({ id, type: "error", message, description });
140
+ }
141
+ })
142
+ .finally(() => {
143
+ if (shouldDismiss) {
144
+ // Toast is still in load state (and will be indefinitely — dismiss it).
145
+ this.dismiss(id);
146
+ id = undefined;
147
+ }
148
+ data.finally?.();
149
+ });
150
+
151
+ const unwrap = () => new Promise<ToastData>((resolve, reject) => originalPromise.then(() => (result[0] === "reject" ? reject(result[1]) : resolve(result[1]))).catch(reject));
152
+
153
+ if (typeof id !== "string" && typeof id !== "number") return { unwrap };
154
+ else return Object.assign(id, { unwrap });
155
+ };
156
+
157
+ custom = (jsx: (id: number | string) => React.ReactElement, data?: ExternalToast) => {
158
+ const id = data?.id || toastsCounter++;
159
+ this.create({ jsx: jsx(id), id, ...data });
160
+ return id;
161
+ };
162
+ };
163
+
164
+ export const ToastState = new Observer();
165
+
166
+ // Bind this to the toast function.
167
+ const toastFunction = (message: titleT, data?: ExternalToast) => {
168
+ const id = data?.id || toastsCounter++;
169
+ ToastState.addToast({
170
+ title: message,
171
+ ...data,
172
+ id
173
+ });
174
+ return id;
175
+ };
176
+
177
+ const isHttpResponse = (data: any): data is Response => {
178
+ return (
179
+ data &&
180
+ typeof data === "object" &&
181
+ "ok" in data &&
182
+ typeof data.ok === "boolean" &&
183
+ "status" in data &&
184
+ typeof data.status === "number"
185
+ );
186
+ };
187
+
188
+ const basicToast = toastFunction;
189
+
190
+ const getHistory = () => ToastState.toasts;
191
+
192
+ // We use `Object.assign` to maintain the correct types as we would lose them otherwise.
193
+ export const toast = Object.assign(
194
+ basicToast,
195
+ {
196
+ success: ToastState.success,
197
+ info: ToastState.info,
198
+ warning: ToastState.warning,
199
+ error: ToastState.error,
200
+ custom: ToastState.custom,
201
+ message: ToastState.message,
202
+ promise: ToastState.promise,
203
+ dismiss: ToastState.dismiss,
204
+ loading: ToastState.loading
205
+ },
206
+ { getHistory }
207
+ );