init
This commit is contained in:
253
uni_modules/nutui-uni/components/popover/index.scss
Normal file
253
uni_modules/nutui-uni/components/popover/index.scss
Normal file
@@ -0,0 +1,253 @@
|
||||
@import "../popup/index";
|
||||
|
||||
.nut-popover {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
word-break: normal;
|
||||
|
||||
.nut-popover-arrow {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 8px solid transparent;
|
||||
|
||||
&-top {
|
||||
bottom: 0;
|
||||
margin-bottom: -8px;
|
||||
border-top-color: $popover-white-background-color;
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
&-bottom {
|
||||
top: 0;
|
||||
margin-top: -8px;
|
||||
border-top-width: 0;
|
||||
border-bottom-color: $popover-white-background-color;
|
||||
|
||||
&.nut-popover-arrow--bottom {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&.nut-popover-arrow--bottom-start {
|
||||
left: 16px;
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
&.nut-popover-arrow--bottom-end {
|
||||
right: 16px;
|
||||
transform: translateX(0%);
|
||||
}
|
||||
}
|
||||
|
||||
&-left {
|
||||
right: 0;
|
||||
margin-right: -8px;
|
||||
border-right-width: 0;
|
||||
border-left-color: $popover-white-background-color;
|
||||
|
||||
&.nut-popover-arrow--left {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&.nut-popover-arrow--left-start {
|
||||
top: 16px;
|
||||
transform: translateY(0%);
|
||||
}
|
||||
|
||||
&.nut-popover-arrow--left-end {
|
||||
bottom: 16px;
|
||||
transform: translateY(0%);
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
left: 0;
|
||||
margin-left: -8px;
|
||||
border-right-color: $popover-white-background-color;
|
||||
border-left-width: 0;
|
||||
|
||||
&.nut-popover-arrow--right {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&.nut-popover-arrow--right-start {
|
||||
top: 16px;
|
||||
transform: translateY(0%);
|
||||
}
|
||||
|
||||
&.nut-popover-arrow--right-end {
|
||||
bottom: 16px;
|
||||
transform: translateY(0%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-popover-content {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
max-height: initial;
|
||||
overflow-y: initial;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 12px #3232331f;
|
||||
opacity: 1;
|
||||
transition: opacity 0.15s,
|
||||
transform 0.15s;
|
||||
|
||||
&-group {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nut-popover-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid $popover-border-bottom-color;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 2px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.nut-popover-item-img {
|
||||
margin-right: 3px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.nut-popover-menu-item-name {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
&.nut-popover-menu-disabled {
|
||||
color: $popover-disable-color;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&--top {
|
||||
.nut-popover-arrow--top {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&--top-end {
|
||||
right: 0;
|
||||
|
||||
.nut-popover-arrow--top-end {
|
||||
right: 16px;
|
||||
transform: translateX(0%);
|
||||
}
|
||||
}
|
||||
|
||||
&--top-start {
|
||||
left: 0;
|
||||
|
||||
.nut-popover-arrow--top-start {
|
||||
left: 16px;
|
||||
transform: translateX(0%);
|
||||
}
|
||||
}
|
||||
|
||||
&--bottom-end {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&--left-end {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&--left-start {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&--right-end {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&--right-start {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-popover--dark {
|
||||
.nut-popover-content {
|
||||
color: $popover-white-background-color;
|
||||
background: $popover-dark-background-color;
|
||||
|
||||
&--bottom,
|
||||
&--bottom-start,
|
||||
&--bottom-end {
|
||||
.nut-popover-arrow {
|
||||
border-bottom-color: $popover-dark-background-color;
|
||||
}
|
||||
}
|
||||
|
||||
&--top,
|
||||
&--top-start,
|
||||
&--top-end {
|
||||
.nut-popover-arrow {
|
||||
border-top-color: $popover-dark-background-color;
|
||||
}
|
||||
}
|
||||
|
||||
&--left,
|
||||
&--left-start,
|
||||
&--left-end {
|
||||
.nut-popover-arrow {
|
||||
border-left-color: $popover-dark-background-color;
|
||||
}
|
||||
}
|
||||
|
||||
&--right,
|
||||
&--right-start,
|
||||
&--right-end {
|
||||
.nut-popover-arrow {
|
||||
border-right-color: $popover-dark-background-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-popover-enter-from,
|
||||
.nut-popover-leave-active {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.nut-popover-enter-active {
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
|
||||
.nut-popover-leave-active {
|
||||
transition-timing-function: ease-in;
|
||||
}
|
||||
|
||||
.nut-popover-content-bg {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 300;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.nut-popover-wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
2
uni_modules/nutui-uni/components/popover/index.ts
Normal file
2
uni_modules/nutui-uni/components/popover/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type * from './popover'
|
||||
export * from './type'
|
||||
85
uni_modules/nutui-uni/components/popover/popover.ts
Normal file
85
uni_modules/nutui-uni/components/popover/popover.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'
|
||||
import { CHOOSE_EVENT, CLOSE_EVENT, OPEN_EVENT, UPDATE_VISIBLE_EVENT } from '../_constants'
|
||||
import { commonProps, isBoolean, isString, makeArrayProp, makeNumberProp, makeNumericProp, makeStringProp, truthProp } from '../_utils'
|
||||
import type { PopoverList, PopoverLocation, PopoverTheme } from './type'
|
||||
|
||||
export const popoverProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 是否展示气泡弹出层
|
||||
*/
|
||||
visible: Boolean,
|
||||
/**
|
||||
* @description 选项列表
|
||||
*/
|
||||
list: makeArrayProp<PopoverList>([]),
|
||||
/**
|
||||
* @description 主题风格,可选值为 dark
|
||||
*/
|
||||
theme: makeStringProp<PopoverTheme>('light'),
|
||||
/**
|
||||
* @description 弹出位置
|
||||
*/
|
||||
location: makeStringProp<PopoverLocation>('bottom'),
|
||||
/**
|
||||
* @description 出现位置的偏移量
|
||||
*/
|
||||
offset: makeArrayProp<number>([0, 12]),
|
||||
/**
|
||||
* @description 小箭头的偏移量
|
||||
*/
|
||||
arrowOffset: makeNumberProp(0),
|
||||
/**
|
||||
* @description 是否显示小箭头
|
||||
*/
|
||||
showArrow: truthProp,
|
||||
/**
|
||||
* @description 动画时长,单位毫秒
|
||||
*/
|
||||
duration: makeNumericProp(300),
|
||||
/**
|
||||
* @description 是否显示遮罩层
|
||||
*/
|
||||
overlay: Boolean,
|
||||
/**
|
||||
* @description 自定义遮罩层类名
|
||||
*/
|
||||
overlayClass: makeStringProp(''),
|
||||
/**
|
||||
* @description 自定义遮罩层样式
|
||||
*/
|
||||
overlayStyle: { type: Object as PropType<CSSProperties> },
|
||||
/**
|
||||
* @description 是否在点击遮罩层后关闭菜单
|
||||
*/
|
||||
closeOnClickOverlay: truthProp,
|
||||
/**
|
||||
* @description 是否在点击选项后关闭
|
||||
*/
|
||||
closeOnClickAction: truthProp,
|
||||
/**
|
||||
* @description 是否在点击外部元素后关闭菜单
|
||||
*/
|
||||
closeOnClickOutside: truthProp,
|
||||
/**
|
||||
* @description 自定义背景色
|
||||
*/
|
||||
bgColor: makeStringProp(''),
|
||||
/**
|
||||
* @description 自定义目标元素 id, 暂不支持该属性
|
||||
* @deprecated
|
||||
*/
|
||||
targetId: makeStringProp(''),
|
||||
}
|
||||
|
||||
export type PopoverProps = ExtractPropTypes<typeof popoverProps>
|
||||
|
||||
export const popoverEmits = {
|
||||
update: (val: boolean) => isBoolean(val),
|
||||
[UPDATE_VISIBLE_EVENT]: (val: boolean) => isBoolean(val),
|
||||
[OPEN_EVENT]: () => true,
|
||||
[CLOSE_EVENT]: () => true,
|
||||
[CHOOSE_EVENT]: (item: any, index: number) => (item instanceof Object) || isString(index),
|
||||
}
|
||||
|
||||
export type PopoverEmits = typeof popoverEmits
|
||||
295
uni_modules/nutui-uni/components/popover/popover.vue
Normal file
295
uni_modules/nutui-uni/components/popover/popover.vue
Normal file
@@ -0,0 +1,295 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import type { ComponentInternalInstance, CSSProperties } from 'vue'
|
||||
import { CHOOSE_EVENT, CLOSE_EVENT, OPEN_EVENT, PREFIX, UPDATE_VISIBLE_EVENT } from '../_constants'
|
||||
import { useRect } from '../_hooks'
|
||||
import { getMainClass, getMainStyle, getRandomId } from '../_utils'
|
||||
import NutIcon from '../icon/icon.vue'
|
||||
import NutPopup from '../popup/popup.vue'
|
||||
import { popoverEmits, popoverProps } from './popover'
|
||||
import type { PopoverRootPosition } from './type'
|
||||
|
||||
const props = defineProps(popoverProps)
|
||||
const emit = defineEmits(popoverEmits)
|
||||
const instance = getCurrentInstance() as ComponentInternalInstance
|
||||
const popoverID = `popoverRef${getRandomId()}`
|
||||
const popoverContentID = `popoverContentRef${getRandomId()}`
|
||||
const showPopup = ref(props.visible)
|
||||
const rootPosition = ref<PopoverRootPosition>()
|
||||
|
||||
const elRect = ref({
|
||||
width: 0,
|
||||
height: 0,
|
||||
})
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName, {
|
||||
[`nut-popover--${props.theme}`]: true,
|
||||
})
|
||||
})
|
||||
const popoverArrow = computed(() => {
|
||||
const prefixCls = 'nut-popover-arrow'
|
||||
const loca = props.location
|
||||
const direction = loca.split('-')[0]
|
||||
return `${prefixCls} ${prefixCls}-${direction} ${prefixCls}--${loca}`
|
||||
})
|
||||
const popoverArrowStyle = computed(() => {
|
||||
const styles: CSSProperties = {}
|
||||
const { bgColor, arrowOffset, location } = props
|
||||
const direction = location.split('-')[0]
|
||||
const skew = location.split('-')[1]
|
||||
const base = 16
|
||||
|
||||
if (bgColor)
|
||||
styles[`border${upperCaseFirst(direction)}Color` as any] = bgColor
|
||||
|
||||
if (props.arrowOffset !== 0) {
|
||||
if (['bottom', 'top'].includes(direction)) {
|
||||
if (!skew)
|
||||
styles.left = `calc(50% + ${arrowOffset}px)`
|
||||
|
||||
if (skew === 'start')
|
||||
styles.left = `${base + arrowOffset}px`
|
||||
|
||||
if (skew === 'end')
|
||||
styles.right = `${base - arrowOffset}px`
|
||||
}
|
||||
|
||||
if (['left', 'right'].includes(direction)) {
|
||||
if (!skew)
|
||||
styles.top = `calc(50% - ${arrowOffset}px)`
|
||||
|
||||
if (skew === 'start')
|
||||
styles.top = `${base - arrowOffset}px`
|
||||
|
||||
if (skew === 'end')
|
||||
styles.bottom = `${base + arrowOffset}px`
|
||||
}
|
||||
}
|
||||
return styles
|
||||
})
|
||||
|
||||
function upperCaseFirst(str: string) {
|
||||
str = str.toLowerCase()
|
||||
str = str.replace(/\b\w+\b/g, word => word.substring(0, 1).toUpperCase() + word.substring(1))
|
||||
return str
|
||||
}
|
||||
|
||||
const getRootPosition = computed(() => {
|
||||
const styles: CSSProperties = {}
|
||||
if (!rootPosition.value) {
|
||||
styles.visibility = 'hidden'
|
||||
return styles
|
||||
}
|
||||
|
||||
const contentWidth = elRect.value.width
|
||||
const contentHeight = elRect.value.height
|
||||
const { width, height, left, top, right } = rootPosition.value
|
||||
const { location, offset } = props
|
||||
const direction = location?.split('-')[0]
|
||||
const skew = location?.split('-')[1]
|
||||
let cross = 0
|
||||
let parallel = 0
|
||||
if (Array.isArray(offset) && offset?.length === 2) {
|
||||
cross += Number(offset[1])
|
||||
parallel += Number(offset[0])
|
||||
}
|
||||
if (width) {
|
||||
if (['bottom', 'top'].includes(direction)) {
|
||||
const h = direction === 'bottom' ? height + cross : -(contentHeight + cross)
|
||||
styles.top = `${top + h}px`
|
||||
|
||||
if (!skew)
|
||||
styles.left = `${-(contentWidth - width) / 2 + left + parallel}px`
|
||||
|
||||
if (skew === 'start')
|
||||
styles.left = `${left + parallel}px`
|
||||
|
||||
if (skew === 'end')
|
||||
styles.left = `${right + parallel}px`
|
||||
}
|
||||
if (['left', 'right'].includes(direction)) {
|
||||
const contentW = direction === 'left' ? -(contentWidth + cross) : width + cross
|
||||
styles.left = `${left + contentW}px`
|
||||
if (!skew)
|
||||
styles.top = `${top - contentHeight / 2 + height / 2 - 4 + parallel}px`
|
||||
|
||||
if (skew === 'start')
|
||||
styles.top = `${top + parallel}px`
|
||||
|
||||
if (skew === 'end')
|
||||
styles.top = `${top + height + parallel}px`
|
||||
}
|
||||
}
|
||||
|
||||
if (elRect.value.width === 0)
|
||||
styles.visibility = 'hidden'
|
||||
else
|
||||
styles.visibility = 'initial'
|
||||
|
||||
return styles
|
||||
})
|
||||
|
||||
const styles = computed(() => {
|
||||
const styles: CSSProperties = {}
|
||||
if (props.bgColor)
|
||||
styles.background = props.bgColor
|
||||
|
||||
return getMainStyle(props, styles)
|
||||
})
|
||||
|
||||
// 获取宽度
|
||||
async function getContentWidth() {
|
||||
uni.createSelectorQuery().selectViewport().scrollOffset((res: any) => {
|
||||
const distance = res.scrollTop
|
||||
|
||||
if (props.targetId) {
|
||||
useRect(props.targetId, instance.root).then((rect) => {
|
||||
rootPosition.value = {
|
||||
width: rect.width!,
|
||||
height: rect.height!,
|
||||
left: rect.left!,
|
||||
top: rect.top! + distance!,
|
||||
right: rect.right!,
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
useRect(popoverID, instance).then((rect) => {
|
||||
rootPosition.value = {
|
||||
width: rect.width!,
|
||||
height: rect.height!,
|
||||
left: rect.left!,
|
||||
top: rect.top! + distance!,
|
||||
right: rect.right!,
|
||||
}
|
||||
})
|
||||
}
|
||||
}).exec()
|
||||
|
||||
setTimeout(() => {
|
||||
getPopoverContentW()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
async function getPopoverContentW() {
|
||||
useRect(popoverContentID, instance).then((rect) => {
|
||||
elRect.value = {
|
||||
width: rect.width!,
|
||||
height: rect.height!,
|
||||
}
|
||||
})
|
||||
}
|
||||
watch(
|
||||
() => props.visible,
|
||||
(value) => {
|
||||
showPopup.value = value
|
||||
if (value) {
|
||||
nextTick(() => {
|
||||
getContentWidth()
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.location,
|
||||
(value) => {
|
||||
getContentWidth()
|
||||
},
|
||||
)
|
||||
function update(val: boolean) {
|
||||
emit('update', val)
|
||||
emit(UPDATE_VISIBLE_EVENT, val)
|
||||
}
|
||||
function openPopover() {
|
||||
update(!props.visible)
|
||||
emit(OPEN_EVENT)
|
||||
}
|
||||
function closePopover() {
|
||||
emit(UPDATE_VISIBLE_EVENT, false)
|
||||
emit(CLOSE_EVENT)
|
||||
}
|
||||
function chooseItem(item: any, index: number) {
|
||||
!item.disabled && emit(CHOOSE_EVENT, item, index)
|
||||
}
|
||||
function clickContent() {
|
||||
if (props.closeOnClickAction)
|
||||
closePopover()
|
||||
}
|
||||
function clickAway() {
|
||||
props.closeOnClickOutside && closePopover()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
getContentWidth()
|
||||
}, 300)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-popover`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
v-if="!targetId"
|
||||
:id="popoverID"
|
||||
class="nut-popover-wrapper"
|
||||
@click="openPopover"
|
||||
>
|
||||
<slot name="reference" />
|
||||
</view>
|
||||
<view :class="classes" :style="getRootPosition">
|
||||
<NutPopup
|
||||
v-model:visible="showPopup"
|
||||
:destroy-on-close="false"
|
||||
:pop-class="`nut-popover-content nut-popover-content--${location}`"
|
||||
:custom-style="styles"
|
||||
:position="`` as any"
|
||||
:transition="`nut-popover` as any"
|
||||
:overlay="overlay"
|
||||
:duration="+duration"
|
||||
:overlay-style="overlayStyle"
|
||||
:overlay-class="overlayClass"
|
||||
:close-on-click-overlay="closeOnClickOverlay"
|
||||
>
|
||||
<view :id="popoverContentID" class="nut-popover-content-group" @click.stop="clickContent">
|
||||
<view v-if="showArrow" :class="popoverArrow" :style="popoverArrowStyle" />
|
||||
<slot name="content" />
|
||||
<view
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
class="nut-popover-menu-item"
|
||||
:class="[item.className, item.disabled && 'nut-popover-menu-disabled']"
|
||||
@click="chooseItem(item, index)"
|
||||
>
|
||||
<NutIcon v-if="item.icon" :name="item.icon" custom-class="nut-popover-item-img" />
|
||||
<view class="nut-popover-menu-item-name">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</NutPopup>
|
||||
|
||||
<view
|
||||
class="nut-popover-content-bg"
|
||||
:class="{ 'nut-hidden': !showPopup }"
|
||||
@touchmove="clickAway"
|
||||
@click="clickAway"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
20
uni_modules/nutui-uni/components/popover/type.ts
Normal file
20
uni_modules/nutui-uni/components/popover/type.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export type PopoverTheme = 'light' | 'dark'
|
||||
|
||||
export interface PopoverRootPosition {
|
||||
width: number
|
||||
height: number
|
||||
left: number
|
||||
top: number
|
||||
right: number
|
||||
}
|
||||
export const popoverLocation
|
||||
= ['bottom', 'top', 'left', 'right', 'top-start', 'top-end', 'bottom-start', 'bottom-end', 'left-start', 'left-end', 'right-start', 'right-end'] as const
|
||||
export type PopoverLocation = (typeof popoverLocation)[number]
|
||||
|
||||
export interface PopoverList {
|
||||
name: string
|
||||
icon?: string
|
||||
disabled?: boolean
|
||||
className?: any
|
||||
[key: PropertyKey]: any
|
||||
}
|
||||
Reference in New Issue
Block a user