Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion sandbox/composables/useSandboxToaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@ import { onBeforeUnmount } from 'vue'
import { ToastManager } from '@/index'

export default function useSandboxToaster() {
const toaster = new ToastManager()
const toasters = Array.from({ length: 20 }).map(() => new ToastManager())
const toaster = toasters.pop()!

toasters.forEach(item => setTimeout(() => item.destroy(), Math.ceil(Math.random() * 10000)))

Array.from({ length: 20 }).forEach(() => {
setTimeout(() => {
const item = new ToastManager()
setTimeout(() => item.destroy(), Math.ceil(Math.random() * 10000))
}, Math.ceil(Math.random() * 10000))
})

onBeforeUnmount(() => {
toaster.destroy()
Expand Down
5 changes: 5 additions & 0 deletions sandbox/pages/SandboxToaster.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@
KToaster
</KButton>
</SandboxSectionComponent>
<SandboxSectionComponent title="destroy manager">
<KButton @click="() => toaster.destroy()">
Destroy
</KButton>
</SandboxSectionComponent>
</div>
</SandboxLayout>
</template>
Expand Down
55 changes: 55 additions & 0 deletions src/components/KToaster/SharedPool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
type Transition<K, T> = (state: 'creating' | 'acquiring' | 'releasing' | 'destroying', key: K, item: T) => T
type Entry<T> = {
value: T
references: Set<symbol>
}
export default class SharedPool<K, T> {
constructor(
protected transition: Transition<K, T>,
protected pool: Map<K, Entry<T>> = new Map(),
) {}

// getter, not init
acquire(key: K, ref: symbol): T {
const create = !this.pool.has(key)
if (create) {
const references = {
value: this.transition('creating', key, {} as T),
references: new Set<symbol>(),
}
this.pool.set(key, references)
}
// there is no way pool/usage.get(item) can be undefined due to using has
// above hence we use ! to avoid typescript
const item = this.pool.get(key)!
if (!create) {
this.transition('acquiring', key, item.value)
}
item.references.add(ref)
return item.value
}

// deleter
release(key: K, ref: symbol) {
if (this.pool.has(key)) {
// there is no way pool/usage.get(item) can be undefined due to using has
// above hence we use ! to avoid typescript
const item = this.pool.get(key)!
item.references.delete(ref)
if (item.references.size === 0) {
this.pool.delete(key)
this.transition('destroying', key, item.value)
} else {
this.transition('releasing', key, item.value)
}
}
}

destroy() {
Array.from(this.pool.entries()).forEach(([key, item]) => {
Array.from(item.references).forEach((ref) => {
this.release(key, ref)
})
})
}
}
156 changes: 105 additions & 51 deletions src/components/KToaster/ToastManager.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { createVNode, render, ref } from 'vue'
import type { Ref, VNode } from 'vue'
import type { Toast, ToasterAppearance, ToasterOptions } from '@/types'
import type { Toast, ToasterOptions } from '@/types'
import { ToasterAppearances } from '@/types'
import KToaster from '@/components/KToaster/KToaster.vue'
import { getUniqueStringId } from '@/utilities'
import SharedPool from './SharedPool'

interface IToastManager {
toasts: Ref<Toast[]>
setTimer(key: string, timeout: number): number
open(args: Record<string, any> | string): void
close(key: string): void
closeAll(): void
destroy(): void
}

const toasterContainerId = 'kongponents-toaster-container'

Expand All @@ -12,43 +22,32 @@ const toasterDefaults = {
appearance: ToasterAppearances.info,
}

const defaultZIndex = 10000
class SSRToastManager implements IToastManager {
public toasts = ref<Toast[]>([])
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setTimer(...args: Parameters<IToastManager['setTimer']>) {
return 0
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public open(...args: Parameters<IToastManager['open']>) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
close(...args: Parameters<IToastManager['close']>) {}
closeAll() {}
public destroy() {}

export default class ToastManager {
}
class DOMToastManager implements IToastManager {
private toastersContainer: HTMLElement | null = null
private toaster: VNode | null = null
public toasts: Ref<Toast[]> = ref<Toast[]>([])

private zIndex: number = defaultZIndex

constructor(options?: ToasterOptions) {
if (options?.zIndex) {
this.zIndex = options.zIndex
}

this.setupToastersContainer()
}
public toasts = ref<Toast[]>([])

private setupToastersContainer(): void {
// For SSR, prevents failing on the build)
if (typeof document === 'undefined') {
console.warn('ToastManager should only be initialized in the browser environment. Docs: https://kongponents.konghq.com/components/toaster.html')

return
}

const toastersContainerEl = document.getElementById(toasterContainerId)
if (toastersContainerEl) {
this.toastersContainer = toastersContainerEl as HTMLElement
} else {
this.toastersContainer = document.createElement('div')
this.toastersContainer.id = toasterContainerId
document.body.appendChild(this.toastersContainer)
}
constructor() {
this.toastersContainer = document.createElement('div')
this.toastersContainer.id = toasterContainerId
document.body.appendChild(this.toastersContainer)

this.toaster = createVNode(KToaster, {
toasterState: this.toasts.value,
zIndex: this.zIndex,
onClose: (key: string) => this.close(key),
})

Expand All @@ -57,20 +56,19 @@ export default class ToastManager {
}
}

setTimer(key: string, timeout: number): number {
setTimer(key: string, timeout: number) {
return window?.setTimeout(() => this.close(key), timeout) || 0
}

public open(args: Record<string, any> | string): void {
this.setupToastersContainer()

public open(args: Record<string, any> | string) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { key, timeoutMilliseconds, appearance, message, title } = args

const toastKey: string = key ? String(key) : getUniqueStringId()
const toastAppearance: ToasterAppearance = (appearance && Object.keys(ToasterAppearances).indexOf(appearance) !== -1) ? appearance : toasterDefaults.appearance
const timer: number = this.setTimer(toastKey, timeoutMilliseconds || toasterDefaults.timeoutMilliseconds)
const toastKey = key ? String(key) : getUniqueStringId()
const toastAppearance = ((appearance && Object.keys(ToasterAppearances).indexOf(appearance) !== -1) ?
appearance : toasterDefaults.appearance) as (keyof typeof ToasterAppearances)
const timer = this.setTimer(toastKey, timeoutMilliseconds || toasterDefaults.timeoutMilliseconds)
const toasterMessage = typeof args === 'string' ? args : message

// Add toaster to state
Expand All @@ -84,29 +82,85 @@ export default class ToastManager {
})
}

close(key: string): void {
const i: number = this.toasts.value?.findIndex(n => key === n.key)
close(key: string) {
const i = this.toasts.value?.findIndex(n => key === n.key)
clearTimeout(this.toasts.value[i]?.timer)
this.toasts.value.splice(i, 1)
}

closeAll(): void {
closeAll() {
this.toasts.value.forEach(toast => clearTimeout(toast?.timer))
this.toasts.value = []
}

public destroy() {
if (this.toastersContainer) {
render(null, this.toastersContainer)
this.toastersContainer.remove()
}
}
}

const pool = new SharedPool<string, IToastManager>((state, id, item) => {
switch (state) {
case 'creating':
// For SSR, prevents failing on the build)
if (typeof document === 'undefined') {
console.warn('ToastManager should only be initialized in the browser environment, all methods are noops. Docs: https://kongponents.konghq.com/components/toaster.html')
return new SSRToastManager()
}

return new DOMToastManager()
case 'acquiring':
return item
case 'releasing':
return item
case 'destroying':
item.destroy()
return item
}
})

export default class ToastManager {
protected sym = Symbol(toasterContainerId)
// public usage
constructor()
/**
* Destroys the ToastManager instance and removes the toasters container element from the DOM
* @param removeToastersContainer - Whether to remove the toasters container element from the DOM (defaults to false)
* @deprecated If you are using options to set zIndex, this never worked as
* expected and doing this is now deprecated. You can remove `options` as an
* argument.
*/
public destroy(removeToastersContainer: boolean = false) {
const toastersContainerEl = document?.getElementById(toasterContainerId)
if (removeToastersContainer && toastersContainerEl) {
render(null, toastersContainerEl)
toastersContainerEl.remove()
}
constructor(options?: ToasterOptions)

// internal usage
constructor(options: ToasterOptions | undefined, manager: IToastManager)
constructor(
options?: ToasterOptions,
protected manager: IToastManager = pool.acquire(toasterContainerId, this.sym),
) {}

get toasts() {
return this.manager.toasts
}

setTimer(...args: Parameters<IToastManager['setTimer']>) {
return this.manager.setTimer(...args)
}

open(...args: Parameters<IToastManager['open']>) {
return this.manager.open(...args)
}

close(...args: Parameters<IToastManager['close']>) {
return this.manager.close(...args)
}

closeAll(...args: Parameters<IToastManager['closeAll']>) {
return this.manager.closeAll(...args)
}

this.toastersContainer = null
this.toaster = null
// eslint-disable-next-line @typescript-eslint/no-unused-vars
destroy(...args: Parameters<IToastManager['destroy']>) {
return pool.release(toasterContainerId, this.sym)
}
}
Loading