vue-toastflow 0.0.4 → 0.0.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.
@@ -1,354 +1,352 @@
1
- <script setup lang="ts">
2
- import { computed, inject, onMounted, onUnmounted, ref, watch } from "vue";
3
- import Toast from "./Toast.vue";
4
- import type {
5
- ToastAnimation,
6
- ToastConfig,
7
- ToastId,
8
- ToastInstance,
9
- ToastPosition,
10
- ToastStore,
11
- } from "toastflow-core";
12
- import { toastStoreKey } from "../symbols";
13
-
14
- const positions: ToastPosition[] = [
15
- "top-left",
16
- "top-center",
17
- "top-right",
18
- "bottom-left",
19
- "bottom-center",
20
- "bottom-right",
21
- ];
22
-
23
- const injectedStore = inject<ToastStore | null>(toastStoreKey, null);
24
- if (!injectedStore) {
25
- throw new Error("[vue-toastflow] Plugin not installed");
26
- }
27
- const store: ToastStore = injectedStore;
28
-
29
- const toasts = ref<ToastInstance[]>([]);
30
-
31
- // event-driven keys
32
- const progressResetMap = ref<Record<ToastId, number>>({});
33
- const duplicateMap = ref<Record<ToastId, number>>({});
34
- const updateMap = ref<Record<ToastId, number>>({});
35
-
36
- let unsubscribeState: (() => void) | null = null;
37
- let unsubscribeEvents: (() => void) | null = null;
38
-
39
- onMounted(function () {
40
- unsubscribeState = store.subscribe(function (state) {
41
- toasts.value = state.toasts;
42
- });
43
-
44
- unsubscribeEvents = store.subscribeEvents(function (event) {
45
- if (event.kind === "timer-reset") {
46
- progressResetMap.value[event.id] = Math.random();
47
- }
48
-
49
- if (event.kind === "duplicate") {
50
- duplicateMap.value[event.id] = Math.random();
51
- }
52
-
53
- if (event.kind === "update") {
54
- updateMap.value[event.id] = Math.random();
55
- }
56
- });
57
- });
58
-
59
- onUnmounted(function () {
60
- if (unsubscribeState) {
61
- unsubscribeState();
62
- }
63
- if (unsubscribeEvents) {
64
- unsubscribeEvents();
65
- }
66
- });
67
-
68
- function getProgressResetKey(id: ToastId): number {
69
- return progressResetMap.value[id] ?? 0;
70
- }
71
-
72
- function getDuplicateKey(id: ToastId): number {
73
- return duplicateMap.value[id] ?? 0;
74
- }
75
-
76
- function getUpdateKey(id: ToastId): number {
77
- return updateMap.value[id] ?? 0;
78
- }
79
-
80
- const grouped = computed(function () {
81
- const byPos: Record<ToastPosition, ToastInstance[]> = {
82
- "top-left": [],
83
- "top-center": [],
84
- "top-right": [],
85
- "bottom-left": [],
86
- "bottom-center": [],
87
- "bottom-right": [],
88
- };
89
-
90
- for (const toast of toasts.value) {
91
- byPos[toast.position].push(toast);
92
- }
93
-
94
- return byPos;
95
- });
96
-
97
- const baseConfig: ToastConfig = store.getConfig();
98
- const stackConfigs = ref<Record<ToastPosition, ToastConfig>>({
99
- "top-left": { ...baseConfig, position: "top-left" },
100
- "top-center": { ...baseConfig, position: "top-center" },
101
- "top-right": { ...baseConfig, position: "top-right" },
102
- "bottom-left": { ...baseConfig, position: "bottom-left" },
103
- "bottom-center": { ...baseConfig, position: "bottom-center" },
104
- "bottom-right": { ...baseConfig, position: "bottom-right" },
105
- });
106
-
107
- const globalZIndex = computed(function () {
108
- if (!toasts.value.length) {
109
- return baseConfig.zIndex;
110
- }
111
- return Math.max(...toasts.value.map((toast) => toast.zIndex));
112
- });
113
-
114
- function stackConfig(position: ToastPosition): ToastConfig {
115
- return stackConfigs.value[position] ?? { ...baseConfig, position };
116
- }
117
-
118
- function stackStyle(position: ToastPosition): Record<string, string> {
119
- const { offset, width } = stackConfig(position);
120
- // Lock the stack width, so it doesn't collapse when leaving items get absolute-positioned
121
- const style: Record<string, string> = { width, maxWidth: "100%" };
122
-
123
- if (position.startsWith("top-")) {
124
- style.top = offset;
125
- }
126
- if (position.startsWith("bottom-")) {
127
- style.bottom = offset;
128
- }
129
-
130
- if (position.endsWith("left")) {
131
- style.left = offset;
132
- } else if (position.endsWith("right")) {
133
- style.right = offset;
134
- } else if (position.endsWith("center")) {
135
- style.left = "50%";
136
- style.transform = "translateX(-50%)";
137
- }
138
-
139
- return style;
140
- }
141
-
142
- function stackAlignClass(position: ToastPosition): string {
143
- if (position.endsWith("left")) {
144
- return "tf-toast-stack--left";
145
- }
146
- if (position.endsWith("center")) {
147
- return "tf-toast-stack--center";
148
- }
149
- return "tf-toast-stack--right";
150
- }
151
-
152
- function stackAxisClass(position: ToastPosition): string | null {
153
- if (position.startsWith("bottom-")) {
154
- return "tf-toast-stack-inner--bottom";
155
- }
156
- return null;
157
- }
158
-
159
- function handleDismiss(id: ToastId) {
160
- store.dismiss(id);
161
- }
162
-
163
- function beforeLeave(el: Element) {
164
- const element = el as HTMLElement;
165
- const parent = element.parentElement;
166
- if (!parent || parent.children.length <= 1) {
167
- return;
168
- }
169
-
170
- const top = element.offsetTop;
171
- const parentHeight = parent.clientHeight;
172
- const parentWidth = parent.clientWidth;
173
-
174
- parent.style.minHeight = `${parentHeight}px`;
175
- element.style.position = "absolute";
176
- element.style.width = `${parentWidth}px`;
177
- element.style.left = "0";
178
- element.style.right = "0";
179
- element.style.top = `${top}px`;
180
- }
181
-
182
- function afterLeave(el: Element) {
183
- const element = el as HTMLElement;
184
- const parent = element.parentElement;
185
- if (parent) {
186
- parent.style.minHeight = "";
187
- }
188
-
189
- element.style.position = "";
190
- element.style.width = "";
191
- element.style.left = "";
192
- element.style.right = "";
193
- element.style.top = "";
194
- }
195
-
196
- watch(
197
- toasts,
198
- function (current) {
199
- const ids = new Set(
200
- current.map(function (toast) {
201
- return toast.id;
202
- }),
203
- );
204
-
205
- for (const key of Object.keys(progressResetMap.value)) {
206
- if (!ids.has(key as ToastId)) {
207
- delete progressResetMap.value[key as ToastId];
208
- }
209
- }
210
-
211
- for (const key of Object.keys(duplicateMap.value)) {
212
- if (!ids.has(key as ToastId)) {
213
- delete duplicateMap.value[key as ToastId];
214
- }
215
- }
216
-
217
- for (const key of Object.keys(updateMap.value)) {
218
- if (!ids.has(key as ToastId)) {
219
- delete updateMap.value[key as ToastId];
220
- }
221
- }
222
- },
223
- { deep: false },
224
- );
225
-
226
- function animationForToast(toast: ToastInstance): Partial<ToastAnimation> {
227
- return {
228
- ...baseConfig.animation,
229
- ...toast.animation,
230
- };
231
- }
232
-
233
- watch(
234
- grouped,
235
- function (byPos) {
236
- const next = { ...stackConfigs.value };
237
-
238
- (Object.keys(byPos) as ToastPosition[]).forEach(function (position) {
239
- const first = byPos[position][0];
240
- if (first) {
241
- next[position] = {
242
- ...baseConfig,
243
- ...first,
244
- position,
245
- animation: {
246
- ...baseConfig.animation,
247
- ...first.animation,
248
- },
249
- };
250
- }
251
- });
252
-
253
- stackConfigs.value = next;
254
- },
255
- { deep: false },
256
- );
257
- </script>
258
-
259
- <template>
260
- <div class="tf-toast-root" :style="{ zIndex: globalZIndex }">
261
- <div
262
- v-for="position in positions"
263
- :key="position"
264
- class="tf-toast-stack"
265
- :class="stackAlignClass(position)"
266
- :style="stackStyle(position)"
267
- >
268
- <TransitionGroup
269
- :name="stackConfig(position).animation.name"
270
- @before-leave="beforeLeave"
271
- @after-leave="afterLeave"
272
- tag="div"
273
- :class="['tf-toast-stack-inner', stackAxisClass(position)]"
274
- :style="{ gap: stackConfig(position).gap }"
275
- >
276
- <div
277
- v-for="toast in grouped[position]"
278
- :key="toast.id"
279
- class="tf-toast-item"
280
- :style="{ width: toast.width, maxWidth: '100%' }"
281
- :data-position="toast.position"
282
- >
283
- <slot
284
- v-if="$slots.default"
285
- :toast="toast"
286
- :progressResetKey="getProgressResetKey(toast.id)"
287
- :duplicateKey="getDuplicateKey(toast.id)"
288
- :updateKey="getUpdateKey(toast.id)"
289
- :bumpAnimationClass="animationForToast(toast).bump"
290
- :clearAllAnimationClass="animationForToast(toast).clearAll"
291
- :updateAnimationClass="animationForToast(toast).update"
292
- :dismiss="handleDismiss"
293
- />
294
-
295
- <Toast
296
- v-else
297
- :toast="toast"
298
- :progressResetKey="getProgressResetKey(toast.id)"
299
- :duplicateKey="getDuplicateKey(toast.id)"
300
- :updateKey="getUpdateKey(toast.id)"
301
- :bumpAnimationClass="animationForToast(toast).bump"
302
- :clearAllAnimationClass="animationForToast(toast).clearAll"
303
- :updateAnimationClass="animationForToast(toast).update"
304
- @dismiss="handleDismiss"
305
- />
306
- </div>
307
- </TransitionGroup>
308
- </div>
309
- </div>
310
- </template>
311
-
312
- <style scoped>
313
- .tf-toast-root {
314
- pointer-events: none;
315
- position: fixed;
316
- inset: 0;
317
- }
318
-
319
- .tf-toast-stack {
320
- position: absolute;
321
- height: 100%;
322
- display: flex;
323
- align-items: flex-start;
324
- }
325
-
326
- .tf-toast-stack-inner {
327
- display: flex;
328
- flex-direction: column;
329
- position: relative;
330
- width: 100%;
331
- }
332
-
333
- .tf-toast-stack-inner--bottom {
334
- min-height: 100%;
335
- justify-content: flex-end;
336
- }
337
-
338
- .tf-toast-stack--left {
339
- justify-content: flex-start;
340
- }
341
-
342
- .tf-toast-stack--center {
343
- justify-content: center;
344
- }
345
-
346
- .tf-toast-stack--right {
347
- justify-content: flex-end;
348
- }
349
-
350
- .tf-toast-item {
351
- pointer-events: auto;
352
- width: 100%;
353
- }
354
- </style>
1
+ <script setup lang="ts">
2
+ import { computed, inject, onMounted, onUnmounted, ref, watch } from "vue";
3
+ import Toast from "./Toast.vue";
4
+ import type {
5
+ ToastAnimation,
6
+ ToastConfig,
7
+ ToastId,
8
+ ToastInstance,
9
+ ToastPosition,
10
+ ToastStore,
11
+ } from "toastflow-core";
12
+ import { toastStoreKey } from "../symbols";
13
+ import { getToastStore } from "../toast";
14
+
15
+ const positions: ToastPosition[] = [
16
+ "top-left",
17
+ "top-center",
18
+ "top-right",
19
+ "bottom-left",
20
+ "bottom-center",
21
+ "bottom-right",
22
+ ];
23
+
24
+ const injectedStore = inject<ToastStore | null>(toastStoreKey, null);
25
+ const store: ToastStore = injectedStore ?? getToastStore();
26
+
27
+ const toasts = ref<ToastInstance[]>([]);
28
+
29
+ // event-driven keys
30
+ const progressResetMap = ref<Record<ToastId, number>>({});
31
+ const duplicateMap = ref<Record<ToastId, number>>({});
32
+ const updateMap = ref<Record<ToastId, number>>({});
33
+
34
+ let unsubscribeState: (() => void) | null = null;
35
+ let unsubscribeEvents: (() => void) | null = null;
36
+
37
+ onMounted(function () {
38
+ unsubscribeState = store.subscribe(function (state) {
39
+ toasts.value = state.toasts;
40
+ });
41
+
42
+ unsubscribeEvents = store.subscribeEvents(function (event) {
43
+ if (event.kind === "timer-reset") {
44
+ progressResetMap.value[event.id] = Math.random();
45
+ }
46
+
47
+ if (event.kind === "duplicate") {
48
+ duplicateMap.value[event.id] = Math.random();
49
+ }
50
+
51
+ if (event.kind === "update") {
52
+ updateMap.value[event.id] = Math.random();
53
+ }
54
+ });
55
+ });
56
+
57
+ onUnmounted(function () {
58
+ if (unsubscribeState) {
59
+ unsubscribeState();
60
+ }
61
+ if (unsubscribeEvents) {
62
+ unsubscribeEvents();
63
+ }
64
+ });
65
+
66
+ function getProgressResetKey(id: ToastId): number {
67
+ return progressResetMap.value[id] ?? 0;
68
+ }
69
+
70
+ function getDuplicateKey(id: ToastId): number {
71
+ return duplicateMap.value[id] ?? 0;
72
+ }
73
+
74
+ function getUpdateKey(id: ToastId): number {
75
+ return updateMap.value[id] ?? 0;
76
+ }
77
+
78
+ const grouped = computed(function () {
79
+ const byPos: Record<ToastPosition, ToastInstance[]> = {
80
+ "top-left": [],
81
+ "top-center": [],
82
+ "top-right": [],
83
+ "bottom-left": [],
84
+ "bottom-center": [],
85
+ "bottom-right": [],
86
+ };
87
+
88
+ for (const toast of toasts.value) {
89
+ byPos[toast.position].push(toast);
90
+ }
91
+
92
+ return byPos;
93
+ });
94
+
95
+ const baseConfig: ToastConfig = store.getConfig();
96
+ const stackConfigs = ref<Record<ToastPosition, ToastConfig>>({
97
+ "top-left": { ...baseConfig, position: "top-left" },
98
+ "top-center": { ...baseConfig, position: "top-center" },
99
+ "top-right": { ...baseConfig, position: "top-right" },
100
+ "bottom-left": { ...baseConfig, position: "bottom-left" },
101
+ "bottom-center": { ...baseConfig, position: "bottom-center" },
102
+ "bottom-right": { ...baseConfig, position: "bottom-right" },
103
+ });
104
+
105
+ const globalZIndex = computed(function () {
106
+ if (!toasts.value.length) {
107
+ return baseConfig.zIndex;
108
+ }
109
+ return Math.max(...toasts.value.map((toast) => toast.zIndex));
110
+ });
111
+
112
+ function stackConfig(position: ToastPosition): ToastConfig {
113
+ return stackConfigs.value[position] ?? { ...baseConfig, position };
114
+ }
115
+
116
+ function stackStyle(position: ToastPosition): Record<string, string> {
117
+ const { offset, width } = stackConfig(position);
118
+ // Lock the stack width, so it doesn't collapse when leaving items get absolute-positioned
119
+ const style: Record<string, string> = { width, maxWidth: "100%" };
120
+
121
+ if (position.startsWith("top-")) {
122
+ style.top = offset;
123
+ }
124
+ if (position.startsWith("bottom-")) {
125
+ style.bottom = offset;
126
+ }
127
+
128
+ if (position.endsWith("left")) {
129
+ style.left = offset;
130
+ } else if (position.endsWith("right")) {
131
+ style.right = offset;
132
+ } else if (position.endsWith("center")) {
133
+ style.left = "50%";
134
+ style.transform = "translateX(-50%)";
135
+ }
136
+
137
+ return style;
138
+ }
139
+
140
+ function stackAlignClass(position: ToastPosition): string {
141
+ if (position.endsWith("left")) {
142
+ return "tf-toast-stack--left";
143
+ }
144
+ if (position.endsWith("center")) {
145
+ return "tf-toast-stack--center";
146
+ }
147
+ return "tf-toast-stack--right";
148
+ }
149
+
150
+ function stackAxisClass(position: ToastPosition): string | null {
151
+ if (position.startsWith("bottom-")) {
152
+ return "tf-toast-stack-inner--bottom";
153
+ }
154
+ return null;
155
+ }
156
+
157
+ function handleDismiss(id: ToastId) {
158
+ store.dismiss(id);
159
+ }
160
+
161
+ function beforeLeave(el: Element) {
162
+ const element = el as HTMLElement;
163
+ const parent = element.parentElement;
164
+ if (!parent || parent.children.length <= 1) {
165
+ return;
166
+ }
167
+
168
+ const top = element.offsetTop;
169
+ const parentHeight = parent.clientHeight;
170
+ const parentWidth = parent.clientWidth;
171
+
172
+ parent.style.minHeight = `${parentHeight}px`;
173
+ element.style.position = "absolute";
174
+ element.style.width = `${parentWidth}px`;
175
+ element.style.left = "0";
176
+ element.style.right = "0";
177
+ element.style.top = `${top}px`;
178
+ }
179
+
180
+ function afterLeave(el: Element) {
181
+ const element = el as HTMLElement;
182
+ const parent = element.parentElement;
183
+ if (parent) {
184
+ parent.style.minHeight = "";
185
+ }
186
+
187
+ element.style.position = "";
188
+ element.style.width = "";
189
+ element.style.left = "";
190
+ element.style.right = "";
191
+ element.style.top = "";
192
+ }
193
+
194
+ watch(
195
+ toasts,
196
+ function (current) {
197
+ const ids = new Set(
198
+ current.map(function (toast) {
199
+ return toast.id;
200
+ }),
201
+ );
202
+
203
+ for (const key of Object.keys(progressResetMap.value)) {
204
+ if (!ids.has(key as ToastId)) {
205
+ delete progressResetMap.value[key as ToastId];
206
+ }
207
+ }
208
+
209
+ for (const key of Object.keys(duplicateMap.value)) {
210
+ if (!ids.has(key as ToastId)) {
211
+ delete duplicateMap.value[key as ToastId];
212
+ }
213
+ }
214
+
215
+ for (const key of Object.keys(updateMap.value)) {
216
+ if (!ids.has(key as ToastId)) {
217
+ delete updateMap.value[key as ToastId];
218
+ }
219
+ }
220
+ },
221
+ { deep: false },
222
+ );
223
+
224
+ function animationForToast(toast: ToastInstance): Partial<ToastAnimation> {
225
+ return {
226
+ ...baseConfig.animation,
227
+ ...toast.animation,
228
+ };
229
+ }
230
+
231
+ watch(
232
+ grouped,
233
+ function (byPos) {
234
+ const next = { ...stackConfigs.value };
235
+
236
+ (Object.keys(byPos) as ToastPosition[]).forEach(function (position) {
237
+ const first = byPos[position][0];
238
+ if (first) {
239
+ next[position] = {
240
+ ...baseConfig,
241
+ ...first,
242
+ position,
243
+ animation: {
244
+ ...baseConfig.animation,
245
+ ...first.animation,
246
+ },
247
+ };
248
+ }
249
+ });
250
+
251
+ stackConfigs.value = next;
252
+ },
253
+ { deep: false },
254
+ );
255
+ </script>
256
+
257
+ <template>
258
+ <div class="tf-toast-root" :style="{ zIndex: globalZIndex }">
259
+ <div
260
+ v-for="position in positions"
261
+ :key="position"
262
+ class="tf-toast-stack"
263
+ :class="stackAlignClass(position)"
264
+ :style="stackStyle(position)"
265
+ >
266
+ <TransitionGroup
267
+ :name="stackConfig(position).animation.name"
268
+ @before-leave="beforeLeave"
269
+ @after-leave="afterLeave"
270
+ tag="div"
271
+ :class="['tf-toast-stack-inner', stackAxisClass(position)]"
272
+ :style="{ gap: stackConfig(position).gap }"
273
+ >
274
+ <div
275
+ v-for="toast in grouped[position]"
276
+ :key="toast.id"
277
+ class="tf-toast-item"
278
+ :style="{ width: toast.width, maxWidth: '100%' }"
279
+ :data-position="toast.position"
280
+ >
281
+ <slot
282
+ v-if="$slots.default"
283
+ :toast="toast"
284
+ :progressResetKey="getProgressResetKey(toast.id)"
285
+ :duplicateKey="getDuplicateKey(toast.id)"
286
+ :updateKey="getUpdateKey(toast.id)"
287
+ :bumpAnimationClass="animationForToast(toast).bump"
288
+ :clearAllAnimationClass="animationForToast(toast).clearAll"
289
+ :updateAnimationClass="animationForToast(toast).update"
290
+ :dismiss="handleDismiss"
291
+ />
292
+
293
+ <Toast
294
+ v-else
295
+ :toast="toast"
296
+ :progressResetKey="getProgressResetKey(toast.id)"
297
+ :duplicateKey="getDuplicateKey(toast.id)"
298
+ :updateKey="getUpdateKey(toast.id)"
299
+ :bumpAnimationClass="animationForToast(toast).bump"
300
+ :clearAllAnimationClass="animationForToast(toast).clearAll"
301
+ :updateAnimationClass="animationForToast(toast).update"
302
+ @dismiss="handleDismiss"
303
+ />
304
+ </div>
305
+ </TransitionGroup>
306
+ </div>
307
+ </div>
308
+ </template>
309
+
310
+ <style scoped>
311
+ .tf-toast-root {
312
+ pointer-events: none;
313
+ position: fixed;
314
+ inset: 0;
315
+ }
316
+
317
+ .tf-toast-stack {
318
+ position: absolute;
319
+ height: 100%;
320
+ display: flex;
321
+ align-items: flex-start;
322
+ }
323
+
324
+ .tf-toast-stack-inner {
325
+ display: flex;
326
+ flex-direction: column;
327
+ position: relative;
328
+ width: 100%;
329
+ }
330
+
331
+ .tf-toast-stack-inner--bottom {
332
+ min-height: 100%;
333
+ justify-content: flex-end;
334
+ }
335
+
336
+ .tf-toast-stack--left {
337
+ justify-content: flex-start;
338
+ }
339
+
340
+ .tf-toast-stack--center {
341
+ justify-content: center;
342
+ }
343
+
344
+ .tf-toast-stack--right {
345
+ justify-content: flex-end;
346
+ }
347
+
348
+ .tf-toast-item {
349
+ pointer-events: auto;
350
+ width: 100%;
351
+ }
352
+ </style>