Skip to content

Commit b25ef3f

Browse files
committed
feat: 🎸 trigger component feature
trigger component feature
1 parent b6baa0d commit b25ef3f

14 files changed

+492
-83
lines changed

src/App.vue

+17-80
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,24 @@
11
<script setup lang="ts">
22
// This starter template is using Vue 3 <script setup> SFCs
33
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
4-
import HelloWorld from './components/HelloWorld.vue'
5-
// import Test from './components/Test'
64
import { AlignCenterIcon } from './icon/align-center/align-center'
75
import { UserIcon } from './icon/user/user'
86
import { InfoCircleIcon } from './icon/info-circle/info-circle'
97
import { ref } from 'vue'
108
const text = ref('')
11-
const confirm = () => {
12-
console.log('confirm!!!')
13-
}
14-
const handleClear = e => {
15-
console.log(e, 'clear')
16-
}
17-
const hanldeBlur = e => {
18-
console.log(e, 'blur')
19-
}
20-
const handleFocus = e => {
21-
console.log(e, 'focus')
22-
}
23-
const handlePressEnter = e => {
24-
console.log(e, 'pressEnter')
25-
}
269
</script>
2710

2811
<template>
29-
<!-- <img alt="Vue logo" src="./assets/logo.png" /> -->
30-
<HelloWorld msg="Hello Sheep UI" />
31-
<!-- <Test>
32-
<template #default> aaaa </template>
33-
<template #title><h3>title</h3></template>
34-
</Test> -->
35-
<!-- 1.type:primary,secondary,text -->
36-
<div>
37-
<SSpace :wrap="true" direction="vertical" fill>
38-
<SButton type="primary">确定</SButton>
39-
<!-- <div>xx</div> -->
40-
<SButton
41-
type="secondary"
42-
tag="a"
43-
href="https://anyway.fm/news.php"
44-
@click="confirm"
45-
>
46-
<template #icon> icon </template>
47-
取消
48-
</SButton>
49-
textNode
50-
<SButton type="text">文本</SButton>
51-
</SSpace>
52-
</div>
53-
<!-- 2.size:small,medium,large -->
54-
<div>
55-
<SButton type="primary" size="small">small</SButton>
56-
<SButton type="primary" size="medium">medium</SButton>
57-
<SButton type="primary" size="large">large</SButton>
58-
</div>
59-
<div>
60-
<SButton type="secondary" size="small">small</SButton>
61-
<SButton type="secondary" size="medium">medium</SButton>
62-
<SButton type="secondary" size="large">large</SButton>
63-
</div>
64-
<!-- 3. disabled -->
65-
<div>
66-
<SButton type="primary" @click="confirm">确定</SButton>
67-
<SButton type="primary" disabled @click="confirm">disabled</SButton>
68-
</div>
69-
<div>
70-
<SButton type="secondary" @click="confirm">确定</SButton>
71-
<SButton type="secondary" disabled @click="confirm">disabled</SButton>
72-
</div>
73-
<div>
74-
<SButton type="text" @click="confirm">确定</SButton>
75-
<SButton type="text" disabled @click="confirm">disabled</SButton>
76-
</div>
77-
<!-- 4.block -->
78-
<SButton type="primary" block>Confirm</SButton>
79-
<SButton type="secondary" block>Cancel</SButton>
80-
<AlignCenterIcon />
81-
<div>
82-
<SInput default-value="xxx" placeholder="请输入你的名字" allow-clear>
83-
<template #prefix>
84-
<UserIcon />
85-
</template>
86-
<template #suffix>
87-
<InfoCircleIcon />
88-
</template>
89-
</SInput>
90-
</div>
91-
<input />
12+
<STrigger
13+
:default-popup-visible="true"
14+
trigger="hover"
15+
:unmount-on-close="true"
16+
>
17+
<SButton>Click me</SButton>
18+
<template #content>
19+
<div class="demo-basic">luanhanxiao is a good man</div>
20+
</template>
21+
</STrigger>
9222
</template>
9323

9424
<style>
@@ -100,4 +30,11 @@ const handlePressEnter = e => {
10030
color: #2c3e50;
10131
margin-top: 60px;
10232
}
33+
.demo-basic {
34+
padding: 10px;
35+
width: 200px;
36+
background-color: var(--color-bg-popup);
37+
border-radius: 4px;
38+
box-shadow: 0 2px 8px #00000026;
39+
}
10340
</style>

src/_hooks/useFirstElement.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ref, onMounted, onUpdated } from 'vue'
2+
import { getFirstElementFromChildren } from '../_utils/vue-utils'
3+
import type { VNode, Slots } from 'vue'
4+
5+
export function useFirstElement(slots: Slots) {
6+
const defaultSlot = slots.default?.() ?? []
7+
const firstElement = ref<HTMLElement>()
8+
9+
const setFirstElement = () => {
10+
const element = getFirstElementFromChildren(defaultSlot)
11+
if (element !== firstElement.value) {
12+
firstElement.value = element
13+
}
14+
}
15+
// setFirstElement()
16+
17+
onMounted(() => {
18+
setFirstElement()
19+
})
20+
21+
onUpdated(() => {
22+
setFirstElement()
23+
})
24+
25+
return {
26+
defaultSlot,
27+
firstElement
28+
}
29+
}

src/_utils/dom.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { isString } from './is'
2+
3+
export const NOOP = () => {
4+
return undefined
5+
}
6+
7+
export const isServerRendering = (() => {
8+
try {
9+
return !(typeof window !== 'undefined' && document !== undefined)
10+
} catch (error) {
11+
return true
12+
}
13+
})()
14+
15+
export function getElement(
16+
target: string | HTMLElement,
17+
container = document
18+
): HTMLElement | undefined {
19+
if (isString(target)) {
20+
return container.querySelector<HTMLElement>(target) ?? undefined
21+
}
22+
return target
23+
}
24+
25+
export const on = (() => {
26+
if (isServerRendering) return NOOP
27+
28+
return (
29+
element: HTMLElement | Window,
30+
event: keyof HTMLElementEventMap,
31+
handler: EventListenerOrEventListenerObject,
32+
options: boolean | AddEventListenerOptions = false
33+
) => {
34+
element.addEventListener(event, handler, options)
35+
}
36+
})()
37+
38+
export const off = (() => {
39+
if (isServerRendering) return NOOP
40+
41+
return (
42+
element: HTMLElement | Window,
43+
event: keyof HTMLElementEventMap,
44+
handler: EventListenerOrEventListenerObject,
45+
options: boolean | AddEventListenerOptions = false
46+
) => {
47+
element.removeEventListener(event, handler, options)
48+
}
49+
})()

src/_utils/is.ts

+8
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,11 @@ export function isArray(obj: any): obj is any[] {
77
export function isNumber(obj: any): obj is number {
88
return opt.call(obj) === '[object Number]' && obj === obj // eslint-disable-line
99
}
10+
11+
export function isFunction(obj: any): obj is (...args: any[]) => any {
12+
return opt.call(obj) === '[object Function]'
13+
}
14+
15+
export function isString(obj: any): obj is string {
16+
return typeof obj === 'string'
17+
}

src/_utils/vue-utils.ts

+62-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { Fragment, Comment } from 'vue'
1+
import { Fragment, Comment, cloneVNode } from 'vue'
22
import type { VNode } from 'vue'
3+
import { isArray, isFunction } from './is'
34

45
export const enum ShapeFlags {
56
ELEMENT = 1,
@@ -14,6 +15,7 @@ export const enum ShapeFlags {
1415
COMPONENT_KEPT_ALIVE = 1 << 9,
1516
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
1617
}
18+
type Data = Record<string, any>
1719

1820
export const isTextNode = (vnode: VNode) =>
1921
vnode && vnode.shapeFlag & ShapeFlags.TEXT_CHILDREN
@@ -26,7 +28,7 @@ export const isElement = (vnode: VNode) =>
2628

2729
export const isSlot = (vnode: VNode) =>
2830
vnode && vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN
29-
export const isArrayNode = (vnode: VNode) =>
31+
export const isArrayChildrenNode = (vnode: VNode) =>
3032
vnode && vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN
3133
export const isFragment = (vnode: VNode) => vnode && vnode.type === Fragment
3234

@@ -50,3 +52,61 @@ export function getAllElements(children: VNode[] | undefined) {
5052

5153
return result
5254
}
55+
56+
export const mergeFirstChild = (
57+
children: VNode[] | undefined,
58+
extraProps: Data | ((vnode?: VNode) => Data)
59+
): boolean => {
60+
if (!children?.length) return false
61+
62+
for (let i = 0; i < children.length; i++) {
63+
const vnode = children[i]
64+
if (isElement(vnode) || isComponent(vnode)) {
65+
const props = isFunction(extraProps) ? extraProps(vnode) : extraProps
66+
children[i] = cloneVNode(vnode, props, true)
67+
return true
68+
}
69+
const _children = getArrayChildren(vnode)
70+
if (_children && _children.length) {
71+
const result = mergeFirstChild(_children, extraProps)
72+
if (result) return result
73+
}
74+
}
75+
76+
return false
77+
}
78+
79+
function getArrayChildren(vnode: VNode): VNode[] | undefined {
80+
if (isArrayChildrenNode(vnode)) return vnode.children as VNode[]
81+
if (isArray(vnode)) return vnode
82+
83+
return undefined
84+
}
85+
86+
export function getFirstElementFromChildren(children: VNode[]) {
87+
for (const child of children) {
88+
const element = getFirstElementFromVnode(child)
89+
if (element) return element
90+
}
91+
return undefined
92+
}
93+
94+
export const getFirstElementFromVnode = (
95+
vnode: VNode
96+
): HTMLElement | undefined => {
97+
if (isElement(vnode)) return vnode.el as HTMLElement
98+
99+
if (isComponent(vnode)) {
100+
if ((vnode.el as Node).nodeType === Node.ELEMENT_NODE)
101+
return vnode.el as HTMLElement
102+
103+
if (vnode.component?.subTree) {
104+
const element = getFirstElementFromVnode(vnode.component?.subTree)
105+
if (element) return element
106+
}
107+
} else {
108+
const children = getArrayChildren(vnode)
109+
if (children) return getFirstElementFromChildren(children)
110+
}
111+
return undefined
112+
}

src/components.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export { default as SButton, ButtonGroup as SButtonGroup } from './button'
22
export { default as SSpace } from './space'
33
export { default as SInput } from './input'
4+
export { default as STrigger } from './trigger'
5+
export { default as STree } from './tree'

src/env.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,12 @@ declare module '*.vue' {
66
const component: DefineComponent<{}, {}, any>
77
export default component
88
}
9+
10+
interface ImportMetaEnv {
11+
readonly VITE_API_TIMEOUT: number
12+
readonly VITE_MOCKUP: boolean
13+
}
14+
15+
interface ImportMeta {
16+
readonly env: ImportMetaEnv
17+
}

src/index.scss

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55

66
@import "button/style/button.scss";
77
@import "space/style/index.scss";
8-
@import "input/style/index.scss";
8+
@import "input/style/index.scss";
9+
@import "trigger/style/index.scss";

src/trigger/index.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { App } from 'vue'
2+
import Trigger from './src/trigger'
3+
import { installComponent } from '../install'
4+
import type { SheepUIOptions } from '../_utils/global-config'
5+
6+
// 具名导出
7+
export { Trigger as STrigger }
8+
9+
// 导出插件
10+
export default {
11+
install(app: App, options?: SheepUIOptions) {
12+
installComponent(app, Trigger, options)
13+
}
14+
}

src/trigger/src/helper.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { CSSProperties } from 'vue'
2+
3+
export function getElementScrollRect(
4+
element: HTMLElement,
5+
containerRect: DOMRect
6+
) {
7+
const rect = element.getBoundingClientRect()
8+
return {
9+
top: rect.top,
10+
bottom: rect.bottom,
11+
left: rect.left,
12+
right: rect.right,
13+
scrollTop: rect.top - containerRect.top,
14+
scrollBottom: rect.bottom - containerRect.top,
15+
scrollLeft: rect.left - containerRect.left,
16+
scrollRight: rect.right - containerRect.left,
17+
width: element.offsetWidth ?? element.clientWidth,
18+
height: element.offsetHeight ?? element.clientHeight
19+
} as unknown as DOMRect
20+
}
21+
22+
export function genPopupStyle(
23+
containerRect: DOMRect,
24+
triggerRect: DOMRect,
25+
popupRect: DOMRect
26+
): CSSProperties {
27+
const popupPositon = genPopupOffset(triggerRect, popupRect)
28+
const style = {
29+
top: popupPositon.top + 'px',
30+
left: popupPositon.left + 'px'
31+
}
32+
return style
33+
}
34+
35+
function genPopupOffset(triggerRect: DOMRect, popupRect: DOMRect) {
36+
// 默认从底部弹出
37+
console.log(triggerRect, popupRect)
38+
return {
39+
top: triggerRect.bottom,
40+
left: triggerRect.left + triggerRect.width / 2 - popupRect.width / 2
41+
}
42+
}

0 commit comments

Comments
 (0)