/
index.ts
203 lines (162 loc) · 4.74 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
const waitFor = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
let instances: Set<Toast> = new Set()
let container: HTMLDivElement
export interface Action {
text: string
callback?: ActionCallback
}
export type Message = string | HTMLElement
export type ActionCallback = (toast: Toast) => void
export interface ToastOptions {
/**
* Automatically destroy the toast in specific timeout (ms)
* @default `0` which means would not automatically destory the toast
*/
timeout?: number
/**
* Toast type
* @default `default`
*/
type?: 'success' | 'error' | 'warning' | 'dark' | 'default'
action?: Action
cancel?: string
}
export class Toast {
message: Message
options: ToastOptions
el?: HTMLDivElement
private timeoutId?: number
constructor(message: Message, options: ToastOptions = {}) {
const { timeout = 0, action, type = 'default', cancel } = options
this.message = message
this.options = {
timeout,
action,
type,
cancel
}
this.setContainer()
this.insert()
instances.add(this)
}
insert(): void {
const el = document.createElement('div')
el.className = 'toast'
el.setAttribute('aria-live', 'assertive')
el.setAttribute('aria-atomic', 'true')
el.setAttribute('aria-hidden', 'false')
const { action, type, cancel } = this.options
const inner = document.createElement('div')
inner.className = 'toast-inner'
const text = document.createElement('div')
text.className = 'toast-text'
inner.classList.add(type as string)
if (typeof this.message === 'string') {
text.textContent = this.message
} else {
text.appendChild(this.message)
}
inner.appendChild(text)
if (cancel) {
const button = document.createElement('button')
button.className = 'toast-button cancel-button'
button.textContent = cancel
button.type = 'text'
button.onclick = () => this.destory()
inner.appendChild(button)
}
if (action) {
const button = document.createElement('button')
button.className = 'toast-button'
button.textContent = action.text
button.type = 'text'
button.onclick = () => {
this.stopTimer()
if (action.callback) {
action.callback(this)
} else {
this.destory()
}
}
inner.appendChild(button)
}
el.appendChild(inner)
this.startTimer()
this.el = el
container.appendChild(el)
// Delay to set slide-up transition
waitFor(50).then(sortToast)
}
destory(): void {
const { el } = this
if (!el) return
container.removeChild(el)
instances.delete(this)
sortToast()
}
setContainer(): void {
container = document.querySelector('.toast-container') as HTMLDivElement
if (!container) {
container = document.createElement('div')
container.className = 'toast-container'
document.body.appendChild(container)
}
// Stop all instance timer when mouse enter
container.addEventListener('mouseenter', () => {
instances.forEach(instance => instance.stopTimer())
})
// Restart all instance timer when mouse leave
container.addEventListener('mouseleave', () => {
instances.forEach(instance => instance.startTimer())
})
}
startTimer(): void {
if (this.options.timeout && !this.timeoutId) {
this.timeoutId = self.setTimeout(
() => this.destory(),
this.options.timeout
)
}
}
stopTimer(): void {
if (this.timeoutId) {
clearTimeout(this.timeoutId)
this.timeoutId = undefined
}
}
}
export function createToast(message: Message, options?: ToastOptions): Toast {
return new Toast(message, options)
}
export function destoryAllToasts(): void {
if (!container) return
instances.clear()
while (container.firstChild) {
container.removeChild(container.firstChild)
}
}
function sortToast(): void {
const toasts = Array.from(instances)
.reverse()
.slice(0, 4)
const heights: Array<number> = []
toasts.forEach((toast, index) => {
const sortIndex = index + 1
const el = toast.el as HTMLDivElement
const height = +(el.getAttribute('data-height') || 0) || el.clientHeight
heights.push(height)
el.className = `toast toast-${sortIndex}`
el.dataset.height = '' + height
el.style.setProperty('--index', '' + sortIndex)
el.style.setProperty('--height', height + 'px')
el.style.setProperty('--front-height', `${heights[0]}px`)
if (sortIndex > 1) {
const hoverOffsetY = heights
.slice(0, sortIndex - 1)
.reduce((res, next) => (res += next), 0)
el.style.setProperty('--hover-offset-y', `-${hoverOffsetY}px`)
} else {
el.style.removeProperty('--hover-offset-y')
}
})
}