@v-c/notification 0.0.2
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/LICENSE +21 -0
- package/bump.config.ts +6 -0
- package/dist/Notice.cjs +230 -0
- package/dist/Notice.d.ts +15 -0
- package/dist/Notice.js +225 -0
- package/dist/NoticeList.cjs +157 -0
- package/dist/NoticeList.d.ts +13 -0
- package/dist/NoticeList.js +154 -0
- package/dist/NotificationProvider.cjs +13 -0
- package/dist/NotificationProvider.d.ts +10 -0
- package/dist/NotificationProvider.js +10 -0
- package/dist/Notifications.cjs +146 -0
- package/dist/Notifications.d.ts +24 -0
- package/dist/Notifications.js +143 -0
- package/dist/_virtual/rolldown_runtime.cjs +21 -0
- package/dist/hooks/useNotification.cjs +80 -0
- package/dist/hooks/useNotification.d.ts +36 -0
- package/dist/hooks/useNotification.js +78 -0
- package/dist/hooks/useStack.cjs +24 -0
- package/dist/hooks/useStack.d.ts +6 -0
- package/dist/hooks/useStack.js +22 -0
- package/dist/index.cjs +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/interface.cjs +0 -0
- package/dist/interface.d.ts +55 -0
- package/dist/interface.js +0 -0
- package/docs/context.vue +34 -0
- package/docs/hooks.vue +89 -0
- package/docs/index.less +265 -0
- package/docs/maxCount.vue +24 -0
- package/docs/motion.ts +33 -0
- package/docs/notification.stories.vue +31 -0
- package/docs/showProgress.vue +34 -0
- package/docs/stack.vue +39 -0
- package/package.json +30 -0
- package/src/Notice.tsx +212 -0
- package/src/NoticeList.tsx +203 -0
- package/src/NotificationProvider.tsx +19 -0
- package/src/Notifications.tsx +164 -0
- package/src/hooks/useNotification.tsx +163 -0
- package/src/hooks/useStack.ts +32 -0
- package/src/index.ts +9 -0
- package/src/interface.ts +61 -0
- package/tsconfig.json +7 -0
- package/vite.config.ts +18 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useNotification } from '../src'
|
|
3
|
+
import motion from './motion.ts'
|
|
4
|
+
import './index.less'
|
|
5
|
+
|
|
6
|
+
const [notice, contextHolder] = useNotification({ showProgress: true, motion })
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<button
|
|
11
|
+
@click="() => {
|
|
12
|
+
notice.open({
|
|
13
|
+
content: `${new Date().toISOString()}`,
|
|
14
|
+
});
|
|
15
|
+
}"
|
|
16
|
+
>
|
|
17
|
+
Show With Progress
|
|
18
|
+
</button>
|
|
19
|
+
<button
|
|
20
|
+
@click="() => {
|
|
21
|
+
notice.open({
|
|
22
|
+
content: `${new Date().toISOString()}`,
|
|
23
|
+
pauseOnHover: false,
|
|
24
|
+
});
|
|
25
|
+
}"
|
|
26
|
+
>
|
|
27
|
+
Not Pause On Hover
|
|
28
|
+
</button>
|
|
29
|
+
<contextHolder />
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<style scoped>
|
|
33
|
+
|
|
34
|
+
</style>
|
package/docs/stack.vue
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useNotification } from '../src'
|
|
3
|
+
import motion from './motion.ts'
|
|
4
|
+
import './index.less'
|
|
5
|
+
|
|
6
|
+
const [{ open }, contextHolder] = useNotification({ motion, stack: true })
|
|
7
|
+
|
|
8
|
+
function getConfig() {
|
|
9
|
+
return {
|
|
10
|
+
content: `${Array.from({ length: Math.round(Math.random() * 5) + 1 })
|
|
11
|
+
.fill(1)
|
|
12
|
+
.map(() => new Date().toISOString())
|
|
13
|
+
.join('\n')}`,
|
|
14
|
+
duration: null,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<button
|
|
21
|
+
@click="() => {
|
|
22
|
+
open(getConfig());
|
|
23
|
+
}"
|
|
24
|
+
>
|
|
25
|
+
Top Right
|
|
26
|
+
</button>
|
|
27
|
+
<button
|
|
28
|
+
@click="() => {
|
|
29
|
+
open({ ...getConfig(), placement: 'bottomRight' });
|
|
30
|
+
}"
|
|
31
|
+
>
|
|
32
|
+
Bottom Right
|
|
33
|
+
</button>
|
|
34
|
+
<contextHolder />
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<style scoped>
|
|
38
|
+
|
|
39
|
+
</style>
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@v-c/notification",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.2",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
},
|
|
14
|
+
"./dist/*": "./dist/*",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"vue": "^3.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@v-c/util": "0.0.16"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "vite build",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"prepublish": "pnpm build",
|
|
28
|
+
"bump": "bumpp --release patch"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/Notice.tsx
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { CSSProperties } from 'vue'
|
|
2
|
+
import type { Key, NoticeConfig } from './interface.ts'
|
|
3
|
+
import { classNames } from '@v-c/util'
|
|
4
|
+
import KeyCode from '@v-c/util/dist/KeyCode'
|
|
5
|
+
import pickAttrs from '@v-c/util/dist/pickAttrs'
|
|
6
|
+
import { computed, defineComponent, shallowRef, watch } from 'vue'
|
|
7
|
+
|
|
8
|
+
export interface NoticeProps extends Omit<NoticeConfig, 'onClose'> {
|
|
9
|
+
prefixCls: string
|
|
10
|
+
eventKey: Key
|
|
11
|
+
onClick?: (event: Event) => void
|
|
12
|
+
onNoticeClose?: (key: Key) => void
|
|
13
|
+
hovering?: boolean
|
|
14
|
+
props?: Record<string, any>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const defaults = {
|
|
18
|
+
duration: 4.5,
|
|
19
|
+
pauseOnHover: true,
|
|
20
|
+
closeIcon: 'x',
|
|
21
|
+
} as const
|
|
22
|
+
|
|
23
|
+
const Notify = defineComponent<NoticeProps & { times?: number }>((props, { attrs }) => {
|
|
24
|
+
const hovering = shallowRef(false)
|
|
25
|
+
const percent = shallowRef(0)
|
|
26
|
+
const spentTime = shallowRef(0)
|
|
27
|
+
|
|
28
|
+
const mergedHovering = computed(() => props.hovering || hovering.value)
|
|
29
|
+
const mergedDuration = computed(() => {
|
|
30
|
+
if (typeof props.duration === 'number') {
|
|
31
|
+
return props.duration
|
|
32
|
+
}
|
|
33
|
+
if (props.duration === undefined) {
|
|
34
|
+
return defaults.duration
|
|
35
|
+
}
|
|
36
|
+
return 0
|
|
37
|
+
})
|
|
38
|
+
const mergedPauseOnHover = computed(() =>
|
|
39
|
+
props.pauseOnHover === undefined ? defaults.pauseOnHover : props.pauseOnHover,
|
|
40
|
+
)
|
|
41
|
+
const mergedShowProgress = computed(() => mergedDuration.value > 0 && props.showProgress)
|
|
42
|
+
const mergedCloseIcon = computed(() => props.closeIcon ?? defaults.closeIcon)
|
|
43
|
+
|
|
44
|
+
// ======================== Close =========================
|
|
45
|
+
const onInternalClose = () => {
|
|
46
|
+
props.onNoticeClose?.(props.eventKey)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const onCloseKeyDown = (e: KeyboardEvent) => {
|
|
50
|
+
if (e.key === 'Enter' || e.code === 'Enter' || e.keyCode === KeyCode.ENTER) {
|
|
51
|
+
onInternalClose()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ======================== Timing ========================
|
|
56
|
+
watch([
|
|
57
|
+
() => props.times,
|
|
58
|
+
mergedDuration,
|
|
59
|
+
mergedHovering,
|
|
60
|
+
], (_n, _, onCleanup) => {
|
|
61
|
+
const duration = mergedDuration.value
|
|
62
|
+
const hoveringValue = mergedHovering.value
|
|
63
|
+
const pauseOnHover = mergedPauseOnHover.value
|
|
64
|
+
if (!hoveringValue && duration > 0) {
|
|
65
|
+
const start = Date.now() - spentTime.value
|
|
66
|
+
const timeoutId = window.setTimeout(() => {
|
|
67
|
+
onInternalClose()
|
|
68
|
+
}, duration * 1000 - spentTime.value)
|
|
69
|
+
|
|
70
|
+
onCleanup(() => {
|
|
71
|
+
if (pauseOnHover) {
|
|
72
|
+
clearTimeout(timeoutId)
|
|
73
|
+
}
|
|
74
|
+
spentTime.value = Date.now() - start
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
}, {
|
|
78
|
+
immediate: true,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// ===================== Progress Bar =====================
|
|
82
|
+
watch([
|
|
83
|
+
() => props.times,
|
|
84
|
+
mergedDuration,
|
|
85
|
+
spentTime,
|
|
86
|
+
mergedHovering,
|
|
87
|
+
mergedShowProgress,
|
|
88
|
+
], (_n, _, onCleanup) => {
|
|
89
|
+
const hoveringValue = mergedHovering.value
|
|
90
|
+
const showProgress = mergedShowProgress.value
|
|
91
|
+
const pauseOnHover = mergedPauseOnHover.value
|
|
92
|
+
const duration = mergedDuration.value
|
|
93
|
+
const baseSpentTime = spentTime.value
|
|
94
|
+
|
|
95
|
+
if (!hoveringValue && showProgress && (pauseOnHover || baseSpentTime === 0)) {
|
|
96
|
+
const start = performance.now()
|
|
97
|
+
let animationFrame = 0
|
|
98
|
+
|
|
99
|
+
const calculate = () => {
|
|
100
|
+
cancelAnimationFrame(animationFrame)
|
|
101
|
+
animationFrame = requestAnimationFrame((timestamp) => {
|
|
102
|
+
const runtime = timestamp + baseSpentTime - start
|
|
103
|
+
const progress = Math.min(runtime / (duration * 1000), 1)
|
|
104
|
+
percent.value = progress * 100
|
|
105
|
+
if (progress < 1) {
|
|
106
|
+
calculate()
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
calculate()
|
|
112
|
+
|
|
113
|
+
onCleanup(() => {
|
|
114
|
+
if (pauseOnHover) {
|
|
115
|
+
cancelAnimationFrame(animationFrame)
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}, {
|
|
120
|
+
immediate: true,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
return () => {
|
|
124
|
+
const {
|
|
125
|
+
closable,
|
|
126
|
+
prefixCls,
|
|
127
|
+
props: divProps,
|
|
128
|
+
onClick,
|
|
129
|
+
content,
|
|
130
|
+
className,
|
|
131
|
+
style,
|
|
132
|
+
} = props
|
|
133
|
+
|
|
134
|
+
// ======================== Closable ========================
|
|
135
|
+
const closableConfig
|
|
136
|
+
= typeof closable === 'object' && closable !== null
|
|
137
|
+
? closable
|
|
138
|
+
: closable
|
|
139
|
+
? { closeIcon: mergedCloseIcon.value }
|
|
140
|
+
: {}
|
|
141
|
+
const ariaProps = pickAttrs(closableConfig, true)
|
|
142
|
+
|
|
143
|
+
// ======================== Progress ========================
|
|
144
|
+
const safePercent = percent.value <= 0 ? 0 : percent.value > 100 ? 100 : percent.value
|
|
145
|
+
const validPercent = 100 - safePercent
|
|
146
|
+
|
|
147
|
+
// ======================== Render ========================
|
|
148
|
+
const noticePrefixCls = `${prefixCls}-notice`
|
|
149
|
+
|
|
150
|
+
const mergedStyle: CSSProperties = {
|
|
151
|
+
...(typeof divProps?.style === 'object' && divProps?.style ? divProps.style : {}),
|
|
152
|
+
...(typeof (attrs as any).style === 'object' && (attrs as any).style ? (attrs as any).style : {}),
|
|
153
|
+
...(typeof style === 'object' && style ? style : {}),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div
|
|
158
|
+
{...divProps}
|
|
159
|
+
class={
|
|
160
|
+
classNames(
|
|
161
|
+
noticePrefixCls,
|
|
162
|
+
className,
|
|
163
|
+
(attrs as any).class,
|
|
164
|
+
{
|
|
165
|
+
[`${noticePrefixCls}-closable`]: !!closable,
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
style={mergedStyle}
|
|
170
|
+
onMouseenter={(e: MouseEvent) => {
|
|
171
|
+
hovering.value = true
|
|
172
|
+
divProps?.onMouseEnter?.(e)
|
|
173
|
+
}}
|
|
174
|
+
onMouseleave={(e: MouseEvent) => {
|
|
175
|
+
hovering.value = false
|
|
176
|
+
divProps?.onMouseLeave?.(e)
|
|
177
|
+
}}
|
|
178
|
+
onClick={onClick}
|
|
179
|
+
>
|
|
180
|
+
{/* Content */}
|
|
181
|
+
<div class={`${noticePrefixCls}-content`}>{content}</div>
|
|
182
|
+
|
|
183
|
+
{/* Close Icon */}
|
|
184
|
+
{closable && (
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
class={`${noticePrefixCls}-close`}
|
|
188
|
+
onKeydown={onCloseKeyDown}
|
|
189
|
+
aria-label="Close"
|
|
190
|
+
{...ariaProps}
|
|
191
|
+
onClick={(e) => {
|
|
192
|
+
e.preventDefault()
|
|
193
|
+
e.stopPropagation()
|
|
194
|
+
onInternalClose()
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
{closableConfig.closeIcon ?? mergedCloseIcon.value}
|
|
198
|
+
</button>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{/* Progress Bar */}
|
|
202
|
+
{mergedShowProgress.value && (
|
|
203
|
+
<progress class={`${noticePrefixCls}-progress`} max="100" value={validPercent}>
|
|
204
|
+
{`${validPercent}%`}
|
|
205
|
+
</progress>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
export default Notify
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { CSSProperties, TransitionGroupProps } from 'vue'
|
|
2
|
+
import type { InnerOpenConfig, Key, NoticeConfig, OpenConfig, Placement, StackConfig } from './interface.ts'
|
|
3
|
+
import { classNames as clsx } from '@v-c/util'
|
|
4
|
+
import { getTransitionGroupProps } from '@v-c/util/dist/utils/transition'
|
|
5
|
+
import { unrefElement } from '@v-c/util/dist/vueuse/unref-element'
|
|
6
|
+
import { computed, defineComponent, reactive, ref, shallowRef, toRef, TransitionGroup, watch, watchEffect } from 'vue'
|
|
7
|
+
import useStack from './hooks/useStack.ts'
|
|
8
|
+
import Notice from './Notice.tsx'
|
|
9
|
+
import { useNotificationContext } from './NotificationProvider.tsx'
|
|
10
|
+
|
|
11
|
+
export interface NoticeListProps {
|
|
12
|
+
configList?: OpenConfig[]
|
|
13
|
+
placement?: Placement
|
|
14
|
+
prefixCls?: string
|
|
15
|
+
motion?: TransitionGroupProps | ((placement: Placement) => TransitionGroupProps)
|
|
16
|
+
stack?: StackConfig
|
|
17
|
+
|
|
18
|
+
// Events
|
|
19
|
+
onAllNoticeRemoved?: (placement: Placement) => void
|
|
20
|
+
onNoticeClose?: (key: Key) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const NoticeList = defineComponent<NoticeListProps>((props, { attrs }) => {
|
|
24
|
+
const ctx = useNotificationContext()
|
|
25
|
+
const dictRef = reactive<Record<string, HTMLDivElement | undefined>>({})
|
|
26
|
+
const keys = computed(() =>
|
|
27
|
+
(props.configList ?? []).map(config => ({
|
|
28
|
+
config,
|
|
29
|
+
key: String(config.key),
|
|
30
|
+
})),
|
|
31
|
+
)
|
|
32
|
+
const latestNotice = shallowRef<HTMLDivElement | null>(null)
|
|
33
|
+
const hoverKeys = ref<string[]>([])
|
|
34
|
+
|
|
35
|
+
const stackConfig = toRef(props, 'stack')
|
|
36
|
+
const [stackEnabled, stackOptions] = useStack(stackConfig)
|
|
37
|
+
const expanded = computed(
|
|
38
|
+
() =>
|
|
39
|
+
stackEnabled.value
|
|
40
|
+
&& (hoverKeys.value.length > 0 || keys.value.length <= stackOptions.threshold!.value!),
|
|
41
|
+
)
|
|
42
|
+
const placementMotion = computed(() => {
|
|
43
|
+
if (typeof props.motion === 'function') {
|
|
44
|
+
return props.placement ? props.motion(props.placement) : undefined
|
|
45
|
+
}
|
|
46
|
+
return props.motion
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Clean hover key
|
|
50
|
+
watch([hoverKeys, keys, stackEnabled], () => {
|
|
51
|
+
if (stackEnabled.value && hoverKeys.value.length > 1) {
|
|
52
|
+
hoverKeys.value = hoverKeys.value.filter(key =>
|
|
53
|
+
keys.value.some(({ key: dataKey }) => key === dataKey),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Sync latest notice after DOM updates so collapsed stack uses accurate height
|
|
59
|
+
watchEffect(
|
|
60
|
+
() => {
|
|
61
|
+
if (!stackEnabled.value) {
|
|
62
|
+
latestNotice.value = null
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const lastKey = keys.value[keys.value.length - 1]?.key
|
|
67
|
+
latestNotice.value = lastKey ? dictRef[lastKey] ?? null : null
|
|
68
|
+
},
|
|
69
|
+
{ flush: 'post' },
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const checkAllClosed = () => {
|
|
73
|
+
if (!props.placement) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (keys.value.length === 0) {
|
|
77
|
+
props.onAllNoticeRemoved?.(props.placement)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
const { prefixCls = '', placement = 'topRight', onNoticeClose } = props
|
|
83
|
+
|
|
84
|
+
const renderNotify = () =>
|
|
85
|
+
keys.value.map(({ config }, motionIndex) => {
|
|
86
|
+
const { key, times } = config as InnerOpenConfig
|
|
87
|
+
const strKey = String(key)
|
|
88
|
+
const {
|
|
89
|
+
className: configClassName,
|
|
90
|
+
style: configStyle,
|
|
91
|
+
classNames: configClassNames,
|
|
92
|
+
styles: configStyles,
|
|
93
|
+
...restConfig
|
|
94
|
+
} = config as NoticeConfig
|
|
95
|
+
const dataIndex = keys.value.findIndex(item => item.key === strKey)
|
|
96
|
+
const stackStyle: CSSProperties = {}
|
|
97
|
+
|
|
98
|
+
if (stackEnabled.value) {
|
|
99
|
+
const index = keys.value.length - 1 - (dataIndex > -1 ? dataIndex : motionIndex - 1)
|
|
100
|
+
const transformX = placement === 'top' || placement === 'bottom' ? '-50%' : '0'
|
|
101
|
+
if (index > 0) {
|
|
102
|
+
stackStyle.height = expanded.value
|
|
103
|
+
? dictRef[strKey]?.offsetHeight
|
|
104
|
+
: latestNotice.value?.offsetHeight
|
|
105
|
+
if (stackStyle.height && typeof stackStyle.height === 'number') {
|
|
106
|
+
stackStyle.height = `${stackStyle.height}px`
|
|
107
|
+
}
|
|
108
|
+
let verticalOffset = 0
|
|
109
|
+
for (let i = 0; i < index; i += 1) {
|
|
110
|
+
const targetKey = keys.value[keys.value.length - 1 - i]?.key
|
|
111
|
+
const node = targetKey ? dictRef[targetKey] : null
|
|
112
|
+
verticalOffset += (node?.offsetHeight ?? 0) + stackOptions.gap!.value!
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const transformY
|
|
116
|
+
= (expanded.value ? verticalOffset : index * stackOptions.offset!.value!)
|
|
117
|
+
* (placement.startsWith('top') ? 1 : -1)
|
|
118
|
+
const currentWidth = dictRef[strKey]?.offsetWidth
|
|
119
|
+
const latestWidth = latestNotice.value?.offsetWidth
|
|
120
|
+
const scaleX
|
|
121
|
+
= !expanded.value && latestWidth && currentWidth
|
|
122
|
+
? (latestWidth - stackOptions.offset!.value! * 2 * (index < 3 ? index : 3))
|
|
123
|
+
/ currentWidth
|
|
124
|
+
: 1
|
|
125
|
+
stackStyle.transform = `translate3d(${transformX}, ${transformY}px, 0) scaleX(${scaleX})`
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
stackStyle.transform = `translate3d(${transformX}, 0, 0)`
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return (
|
|
132
|
+
<div
|
|
133
|
+
key={strKey}
|
|
134
|
+
class={clsx(`${prefixCls}-notice-wrapper`, configClassNames?.wrapper)}
|
|
135
|
+
style={{
|
|
136
|
+
...stackStyle,
|
|
137
|
+
...configStyles?.wrapper,
|
|
138
|
+
}}
|
|
139
|
+
onMouseenter={() => {
|
|
140
|
+
hoverKeys.value = hoverKeys.value.includes(strKey)
|
|
141
|
+
? hoverKeys.value
|
|
142
|
+
: [...hoverKeys.value, strKey]
|
|
143
|
+
}}
|
|
144
|
+
onMouseleave={() => {
|
|
145
|
+
hoverKeys.value = hoverKeys.value.filter(k => k !== strKey)
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
<Notice
|
|
149
|
+
{...restConfig as any}
|
|
150
|
+
ref={(el) => {
|
|
151
|
+
const element = unrefElement<HTMLDivElement>(el as any) ?? undefined
|
|
152
|
+
if (dataIndex > -1) {
|
|
153
|
+
dictRef[strKey] = element
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
delete dictRef[strKey]
|
|
157
|
+
}
|
|
158
|
+
}}
|
|
159
|
+
prefixCls={prefixCls}
|
|
160
|
+
classNames={configClassNames}
|
|
161
|
+
styles={configStyles}
|
|
162
|
+
class={clsx(configClassName, (ctx.value as any)?.classNames?.notice)}
|
|
163
|
+
style={configStyle}
|
|
164
|
+
times={times}
|
|
165
|
+
eventKey={key}
|
|
166
|
+
onNoticeClose={onNoticeClose}
|
|
167
|
+
hovering={stackEnabled.value && hoverKeys.value.length > 0}
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
)
|
|
171
|
+
})
|
|
172
|
+
let motionGroupProps: TransitionGroupProps = {}
|
|
173
|
+
if (placementMotion.value) {
|
|
174
|
+
motionGroupProps = getTransitionGroupProps(placementMotion.value.name!, placementMotion.value)
|
|
175
|
+
}
|
|
176
|
+
return (
|
|
177
|
+
<TransitionGroup
|
|
178
|
+
key={placement}
|
|
179
|
+
tag="div"
|
|
180
|
+
appear
|
|
181
|
+
{...{
|
|
182
|
+
class: clsx(
|
|
183
|
+
prefixCls,
|
|
184
|
+
`${prefixCls}-${placement}`,
|
|
185
|
+
(ctx.value as any)?.classNames?.list,
|
|
186
|
+
(attrs as any).class,
|
|
187
|
+
{
|
|
188
|
+
[`${prefixCls}-stack-expanded`]: expanded.value,
|
|
189
|
+
[`${prefixCls}-stack`]: stackEnabled.value,
|
|
190
|
+
},
|
|
191
|
+
),
|
|
192
|
+
style: (attrs as any).style,
|
|
193
|
+
}}
|
|
194
|
+
{...motionGroupProps}
|
|
195
|
+
onAfterLeave={checkAllClosed}
|
|
196
|
+
>
|
|
197
|
+
{renderNotify()}
|
|
198
|
+
</TransitionGroup>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
export default NoticeList
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { InjectionKey, Ref } from 'vue'
|
|
2
|
+
import { inject, provide, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface NotificationContextProps {
|
|
5
|
+
classNames?: {
|
|
6
|
+
notice?: string
|
|
7
|
+
list?: string
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export const NotificationContext: InjectionKey<Ref<NotificationContextProps>> = Symbol('NotificationContext')
|
|
11
|
+
|
|
12
|
+
export function useNotificationProvider(props: Ref<NotificationContextProps>) {
|
|
13
|
+
provide(NotificationContext, props)
|
|
14
|
+
return props
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useNotificationContext() {
|
|
18
|
+
return inject(NotificationContext, ref({}))
|
|
19
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { VueNode } from '@v-c/util/dist/type'
|
|
2
|
+
import type { CSSProperties, TransitionGroupProps } from 'vue'
|
|
3
|
+
import type { InnerOpenConfig, Key, OpenConfig, Placement, Placements, StackConfig } from './interface.ts'
|
|
4
|
+
import { defineComponent, shallowRef, Teleport, watch } from 'vue'
|
|
5
|
+
import NoticeList from './NoticeList.tsx'
|
|
6
|
+
|
|
7
|
+
export interface NotificationsProps {
|
|
8
|
+
prefixCls?: string
|
|
9
|
+
motion?: TransitionGroupProps | ((placement: Placement) => TransitionGroupProps)
|
|
10
|
+
container?: HTMLElement | ShadowRoot
|
|
11
|
+
maxCount?: number
|
|
12
|
+
className?: (placement: Placement) => string
|
|
13
|
+
style?: (placement: Placement) => CSSProperties
|
|
14
|
+
onAllRemoved?: VoidFunction
|
|
15
|
+
stack?: StackConfig
|
|
16
|
+
renderNotifications?: (
|
|
17
|
+
node: VueNode,
|
|
18
|
+
info: { prefixCls: string, key: Key },
|
|
19
|
+
) => VueNode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface NotificationsRef {
|
|
23
|
+
open: (config: OpenConfig) => void
|
|
24
|
+
close: (key: Key) => void
|
|
25
|
+
destroy: () => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const defaults = {
|
|
29
|
+
prefixCls: 'vc-notification',
|
|
30
|
+
} as NotificationsProps
|
|
31
|
+
|
|
32
|
+
const Notifications = defineComponent<NotificationsProps>(
|
|
33
|
+
(props = defaults, { expose }) => {
|
|
34
|
+
const configList = shallowRef<OpenConfig[]>([])
|
|
35
|
+
// ======================== Close =========================
|
|
36
|
+
const onNoticeClose = (key: Key) => {
|
|
37
|
+
// Trigger close event
|
|
38
|
+
const config = configList.value.find(item => item.key === key)
|
|
39
|
+
const closable = config?.closable
|
|
40
|
+
const closableObj = closable && typeof closable === 'object' ? closable : {}
|
|
41
|
+
closableObj.onClose?.()
|
|
42
|
+
config?.onClose?.()
|
|
43
|
+
configList.value = configList.value.filter(item => item.key !== key)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ========================= Refs =========================
|
|
47
|
+
expose({
|
|
48
|
+
open: (config: OpenConfig) => {
|
|
49
|
+
const list = configList.value
|
|
50
|
+
let clone = [...configList.value]
|
|
51
|
+
// Replace if exist
|
|
52
|
+
const index = clone.findIndex(item => item.key === config.key)
|
|
53
|
+
const innerConfig: InnerOpenConfig = {
|
|
54
|
+
...config,
|
|
55
|
+
}
|
|
56
|
+
if (index >= 0) {
|
|
57
|
+
innerConfig.times = ((list[index] as InnerOpenConfig)?.times || 0) + 1
|
|
58
|
+
clone[index] = innerConfig
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
innerConfig.times = 0
|
|
62
|
+
clone.push(innerConfig)
|
|
63
|
+
}
|
|
64
|
+
const maxCount = props.maxCount ?? 0
|
|
65
|
+
if (maxCount > 0 && clone.length > maxCount) {
|
|
66
|
+
clone = clone.slice(-maxCount)
|
|
67
|
+
}
|
|
68
|
+
configList.value = clone
|
|
69
|
+
},
|
|
70
|
+
close: onNoticeClose,
|
|
71
|
+
destroy: () => {
|
|
72
|
+
configList.value = []
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// ====================== Placements ======================
|
|
77
|
+
|
|
78
|
+
const placements = shallowRef<Placements>({})
|
|
79
|
+
|
|
80
|
+
watch(
|
|
81
|
+
configList,
|
|
82
|
+
() => {
|
|
83
|
+
const nextPlacements: Placements = {}
|
|
84
|
+
configList.value.forEach((config) => {
|
|
85
|
+
const { placement = 'topRight' } = config
|
|
86
|
+
if (placement) {
|
|
87
|
+
nextPlacements[placement] = nextPlacements[placement] || []
|
|
88
|
+
nextPlacements[placement].push(config)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
// Fill exist placements to avoid empty list causing remove without motion
|
|
92
|
+
Object.keys(placements.value).forEach((_placement) => {
|
|
93
|
+
const placement = _placement as Placement
|
|
94
|
+
nextPlacements[placement] = nextPlacements[placement] || []
|
|
95
|
+
})
|
|
96
|
+
placements.value = nextPlacements
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// Clean up container if all notices fade out
|
|
101
|
+
const onAllNoticeRemoved = (placement: Placement) => {
|
|
102
|
+
const clone = { ...placements.value }
|
|
103
|
+
const list = clone[placement] || []
|
|
104
|
+
if (!list.length) {
|
|
105
|
+
delete clone[placement]
|
|
106
|
+
}
|
|
107
|
+
placements.value = clone
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Effect tell that placements is empty now
|
|
111
|
+
const emptyRef = shallowRef(false)
|
|
112
|
+
|
|
113
|
+
watch(
|
|
114
|
+
placements,
|
|
115
|
+
() => {
|
|
116
|
+
if (Object.keys(placements.value).length > 0) {
|
|
117
|
+
emptyRef.value = true
|
|
118
|
+
}
|
|
119
|
+
else if (emptyRef.value) {
|
|
120
|
+
// Trigger only when from exist to empty
|
|
121
|
+
props?.onAllRemoved?.()
|
|
122
|
+
emptyRef.value = false
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return () => {
|
|
128
|
+
const { container } = props
|
|
129
|
+
const prefixCls = props.prefixCls ?? defaults.prefixCls ?? ''
|
|
130
|
+
// ======================== Render ========================
|
|
131
|
+
if (!container) {
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<Teleport to={container}>
|
|
137
|
+
{Object.keys(placements.value).map((placement) => {
|
|
138
|
+
const placementConfigList = placements.value[placement as Placement]
|
|
139
|
+
const list = (
|
|
140
|
+
<NoticeList
|
|
141
|
+
key={placement}
|
|
142
|
+
configList={placementConfigList}
|
|
143
|
+
placement={placement as Placement}
|
|
144
|
+
prefixCls={prefixCls}
|
|
145
|
+
class={props.className?.(placement as Placement)}
|
|
146
|
+
style={props.style?.(placement as Placement)}
|
|
147
|
+
motion={props.motion}
|
|
148
|
+
stack={props.stack}
|
|
149
|
+
onAllNoticeRemoved={() => onAllNoticeRemoved(placement as Placement)}
|
|
150
|
+
onNoticeClose={onNoticeClose}
|
|
151
|
+
/>
|
|
152
|
+
)
|
|
153
|
+
return props.renderNotifications ? props.renderNotifications(list, { prefixCls, key: placement }) : list
|
|
154
|
+
})}
|
|
155
|
+
</Teleport>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'Notifications',
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
export default Notifications
|