This commit is contained in:
2022-11-14 11:56:21 +08:00
commit 0a63adba99
337 changed files with 25661 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
import { Ref } from "vue";
// 计算各个方向位置
const postionFns: any = {
top(
el: HTMLElement,
popper: HTMLElement,
innnerPosition: Ref,
called: boolean
) {
let { top, left, bottom } = el.getBoundingClientRect();
if (
(top = top - popper.offsetHeight - 6) < 0 &&
bottom > popper.offsetHeight
) {
innnerPosition.value = "bottom";
top = bottom;
} else {
innnerPosition.value = "top";
}
return {
top: `${top}px`,
left: `${left - (popper.offsetWidth - el.offsetWidth) / 2}px`,
};
},
bottom(
el: HTMLElement,
popper: HTMLElement,
innnerPosition: Ref,
called: boolean
) {
let { top, left, bottom } = el.getBoundingClientRect();
if (window.innerHeight - bottom < popper.offsetHeight + 6) {
innnerPosition.value = "top";
bottom = top - popper.offsetHeight - 6;
} else {
innnerPosition.value = "bottom";
}
return {
top: `${bottom}px`,
left: `${left - (popper.offsetWidth - el.offsetWidth) / 2}px`,
};
},
left(
el: HTMLElement,
popper: HTMLElement,
innnerPosition: Ref,
called: boolean
) {
let { top, left, right } = el.getBoundingClientRect();
left = left - popper.offsetWidth - 6;
if (left < 0) {
innnerPosition.value = "right";
left = right;
} else {
innnerPosition.value = "left";
}
return {
top: `${top - (popper.offsetHeight - el.offsetHeight) / 2}px`,
left: `${left}px`,
};
},
right(
el: HTMLElement,
popper: HTMLElement,
innnerPosition: Ref,
called: boolean
) {
let { top, left, right } = el.getBoundingClientRect();
if (window.innerWidth < right + popper.offsetWidth + 6) {
innnerPosition.value = "left";
right = left - popper.offsetWidth - 6;
} else {
innnerPosition.value = "right";
}
return {
top: `${top - (popper.offsetHeight - el.offsetHeight) / 2}px`,
left: `${right}px`,
};
},
};
export default postionFns;

View File

@@ -0,0 +1,126 @@
// 主题颜色
// 浅色 --> 默认使用
@ligh-background: #FFF;
@ligh-color: #3a3a3a;
// 深色
@dark-background: #353535;
@dark-color: #FFF;
@border-clor: #cecece;
// 单一设置主题
.single-theme(@position, @contrary_position, @margin_postion, @color, @m-border-color) {
@attr : ~'[position=@{position}]';
&{
border: 1px solid @m-border-color;
&@{attr}{
margin-@{contrary_position}: 6px;
.layui-popper-arrow {
@{contrary_position}: -6px;
border-@{contrary_position}-width: 0;
border-@{position}-color: @m-border-color;
&::after{
@{contrary_position}: 1px;
border-@{contrary_position}-width: 0;
margin-@{margin_postion}: -6px;
border-@{position}-color: @color;
}
}
}
}
}
// 统一设置四个方向的主题
.theme(@background-color, @color, @border-color) {
background-color: @background-color;
color: @color;
.single-theme(top, bottom, left, @background-color, @border-color);
.single-theme(bottom, top, left, @background-color, @border-color);
.single-theme(right, left, top, @background-color, @border-color);
.single-theme(left, right, top, @background-color, @border-color);
}
// 箭头默认居中
.arrow-default-center(@position, @prop) {
@attr : ~'[position=@{position}]';
&@{attr} {
.layui-popper-arrow{
@{prop}: -moz-calc(50% - 6px);
@{prop}: -webkit-calc(50% - 6px);
@{prop}: calc(50% - 6px);
}
}
}
.all-arrow-default-center() {
.arrow-default-center(top, left);
.arrow-default-center(bottom, left);
.arrow-default-center(left, top);
.arrow-default-center(right, top);
}
// 填充popper支持可以移动到popper使用到
.single-fill-popper(@position, @contrary_position, @zeroPosition, @all, @seven){
@attr : ~'[position=@{position}]';
&@{attr}::after{
@{contrary_position}: -7px;
@{zeroPosition}: 0;
@{all}: 100%;
@{seven}: 7px;
}
}
.fill-popper(){
.single-fill-popper(top, bottom, left, width, height);
.single-fill-popper(bottom, top, left, width, height);
.single-fill-popper(left, right, bottom, height, width);
.single-fill-popper(right, left, bottom, height, width);
}
// 样式开始
.layui-popper {
position: fixed;
padding: 10px;
border-radius: 3px;
word-wrap: break-word;
min-width: 12px;
min-height: 12px;
font-size: 14px;
box-sizing: border-box;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.15);
.theme(@ligh-background, @ligh-color, @border-clor);
max-width: 300px;
z-index: 99999;
// 箭头默认居中
.all-arrow-default-center();
&::after{
content: " ";
position: absolute;
display: block;
}
// 填充
.fill-popper();
.layui-popper-arrow {
&,&::after{
position: absolute;
display: block;
width: 0;
height: 0;
border-width: 6px;
border-style: solid;
border-color: transparent;
}
&::after{
content: ' ';
}
}
/* 深色主题 */
&.layui-dark {
.theme(@dark-background, @dark-color, @dark-background);
}
}

View File

@@ -0,0 +1,276 @@
<template>
<teleport to="body" v-if="isExist">
<transition v-show="innerVisible">
<div
ref="popperRefEl"
:class="['layui-popper', { 'layui-dark': isDark }, props.popperClass]"
:style="[style, props.popperStyle ?? '']"
:position="innnerPosition"
@mouseenter="handlerPopperMouseEnter"
@mouseleave="handlerPopperMouseLeave"
>
<slot> {{ content }}</slot>
<div class="layui-popper-arrow"></div>
</div>
</transition>
</teleport>
</template>
<script lang="ts">
const NAME = "LayPopper";
export default {
name: NAME,
};
</script>
<script setup lang="ts">
import "./index.less";
import postionFns from "./calcPosition";
import {
CSSProperties,
ref,
watch,
onMounted,
nextTick,
useSlots,
shallowRef,
computed,
toRef,
StyleValue,
Ref,
} from "vue";
import {
onClickOutside,
useEventListener,
useResizeObserver,
useThrottleFn,
} from "@vueuse/core";
export type PopperTrigger = "click" | "hover" | "focus" | "contextMenu";
export interface PopperProps {
el: HTMLElement;
position?: string;
enterable?: boolean;
isDark?: boolean;
disabled?: boolean;
isCanHide?: boolean;
isAutoShow?: boolean;
visible?: boolean;
content?: string | Number;
trigger?: PopperTrigger | PopperTrigger[];
popperClass?: string | Array<string | object> | object;
popperStyle?: StyleValue;
}
const props = withDefaults(defineProps<PopperProps>(), {
position: "top",
isDark: true,
disabled: false,
enterable: true,
isCanHide: true,
isAutoShow: false,
trigger: "hover",
visible: false,
});
const slots = useSlots();
const style = ref<CSSProperties>({ top: -window.innerHeight + "px", left: 0 });
const triggerRefEl = toRef(props, "el");
const popperRefEl = shallowRef<HTMLDivElement>({} as HTMLDivElement);
const innnerPosition = ref(props.position);
const innerVisible = ref(props.visible);
const isExist = ref(props.visible);
let scrollElements: HTMLElement[] | undefined;
const triggerMethods = computed(() =>
([] as Array<PopperTrigger>).concat(props.trigger)
);
const doShow = function () {
if (!props.disabled) {
if (!isExist.value) {
isExist.value = true;
nextTick(() => {
innerVisible.value = true;
});
} else {
innerVisible.value = true;
}
}
};
const doHidden = function (e?: MouseEvent) {
// isCanHide参数由外控制
if (props.isCanHide === false) {
return;
}
innerVisible.value = false;
innnerPosition.value = props.position;
style.value = { top: -window.innerHeight + "px", left: 0 };
};
const calcPosistion = function () {
postionFns[props.position] &&
(style.value = postionFns[props.position](
triggerRefEl.value,
popperRefEl.value,
innnerPosition
));
};
const updatePosistion = function () {
if (innerVisible.value) {
popperRefEl.value.offsetWidth === 0
? nextTick(() => calcPosistion())
: calcPosistion();
nextTick(() => {
calcPosistion();
});
}
};
const handlerPopperMouseEnter = function () {
if (triggerMethods.value.includes("hover") && props.enterable) {
doShow();
}
};
const handlerPopperMouseLeave = function () {
if (triggerMethods.value.includes("hover") && props.enterable) {
doHidden();
}
};
const handlerTriggerClick = function () {
if (!triggerMethods.value.includes("click")) return;
if (innerVisible.value) {
doHidden();
} else {
doShow();
}
};
const handleTriggerContextMenu = function (e: MouseEvent) {
if (!triggerMethods.value.includes("contextMenu")) return;
e.preventDefault();
if (innerVisible.value) {
doHidden();
} else {
doShow();
}
};
const handlerTriggerMouseEnter = function () {
if (!triggerMethods.value.includes("hover")) return;
doShow();
};
const handlerTriggerMouseLeave = function () {
if (!triggerMethods.value.includes("hover")) return;
doHidden();
};
const handleTriggerFocusin = function () {
if (triggerMethods.value.includes("focus") && props.enterable) {
doShow();
}
};
const handleTriggerFocusout = function () {
if (triggerMethods.value.includes("focus") && props.enterable) {
doHidden();
}
};
const handlerTriggerEventRegist = function () {
useEventListener(triggerRefEl.value, "click", handlerTriggerClick);
useEventListener(triggerRefEl.value, "contextmenu", handleTriggerContextMenu);
useEventListener(triggerRefEl.value, "mouseenter", handlerTriggerMouseEnter);
useEventListener(triggerRefEl.value, "mouseleave", handlerTriggerMouseLeave);
useEventListener(triggerRefEl.value, "focusin", handleTriggerFocusin);
useEventListener(triggerRefEl.value, "focusout", handleTriggerFocusout);
};
const handleScroll = useThrottleFn(() => {
if (innerVisible.value) {
updatePosistion();
}
}, 15);
onClickOutside(
triggerRefEl.value,
(e) => {
if (
!innerVisible.value ||
triggerRefEl.value.contains(e.target as HTMLElement) ||
popperRefEl.value.contains(e.target as HTMLElement)
) {
return;
}
doHidden();
},
{
ignore: [popperRefEl.value],
}
);
useResizeObserver(triggerRefEl, () => {
updatePosistion();
});
let popperObserver: { stop: any; isSupported?: Ref<boolean> } | undefined =
undefined;
watch(innerVisible, (isShow) => {
updatePosistion();
if (isShow) {
popperObserver = useResizeObserver(popperRefEl, () => {
updatePosistion();
});
} else {
popperObserver && popperObserver.stop();
}
});
watch(
() => props.visible,
(isShow) => (isShow ? doShow() : doHidden())
);
watch(
() => props.content,
() => {
updatePosistion();
}
);
const isScrollElement = function (element: HTMLElement) {
return (
element.scrollHeight > element.offsetHeight ||
element.scrollWidth > element.offsetWidth
);
};
const getScrollElements = function (container: HTMLElement | undefined) {
const scrollElements: HTMLElement[] = [];
let element: HTMLElement | undefined = container;
while (element && element !== document.documentElement) {
if (isScrollElement(element)) {
scrollElements.push(element);
}
element = element.parentElement ?? undefined;
}
return scrollElements;
};
onMounted(() => {
updatePosistion();
scrollElements = getScrollElements(triggerRefEl.value);
for (const item of scrollElements) {
useEventListener(item, "scroll", handleScroll);
}
useEventListener("resize", handleScroll);
handlerTriggerEventRegist();
});
</script>