✨ 新增input-number(数字输入框)、tooltip(文字提示)、修改input组件可接受number类型值
This commit is contained in:
parent
e545b9221a
commit
961eac0127
139
example/docs/zh-CN/components/inputNumber.md
Normal file
139
example/docs/zh-CN/components/inputNumber.md
Normal file
@ -0,0 +1,139 @@
|
||||
::: title 基础使用
|
||||
:::
|
||||
|
||||
::: demo
|
||||
|
||||
<template>
|
||||
<lay-input-number v-model="data"></lay-input-number>
|
||||
<lay-input-number v-model="data2" position="right"></lay-input-number>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
const data = ref(0);
|
||||
const data2 = ref(0);
|
||||
export default {
|
||||
setup() {
|
||||
return {
|
||||
data,
|
||||
data2,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
:::
|
||||
|
||||
::: title 尺寸
|
||||
:::
|
||||
|
||||
::: demo
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<lay-input-number></lay-input-number>
|
||||
<lay-input-number size="md"></lay-input-number>
|
||||
<lay-input-number size="sm"></lay-input-number>
|
||||
<lay-input-number size="xs"></lay-input-number>
|
||||
</div>
|
||||
<div>
|
||||
<lay-input-number position="right"></lay-input-number>
|
||||
<lay-input-number position="right" size="md"></lay-input-number>
|
||||
<lay-input-number position="right" size="sm"></lay-input-number>
|
||||
<lay-input-number position="right" size="xs"></lay-input-number>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
|
||||
:::
|
||||
|
||||
::: title 限制数字大小
|
||||
:::
|
||||
|
||||
::: demo
|
||||
|
||||
<template>
|
||||
<lay-input-number :min="0" :max="10"></lay-input-number>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
|
||||
:::
|
||||
|
||||
::: title 数字步数
|
||||
:::
|
||||
|
||||
::: demo
|
||||
|
||||
<template>
|
||||
<lay-input-number :step="10"></lay-input-number>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
|
||||
:::
|
||||
|
||||
::: title 禁用
|
||||
:::
|
||||
|
||||
::: demo
|
||||
|
||||
<template>
|
||||
<p>禁用输入</p>
|
||||
<lay-input-number v-model="data" disabled-input></lay-input-number>
|
||||
<p>全部禁用</p>
|
||||
<lay-input-number v-model="data2" disabled></lay-input-number>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
const data = ref(10);
|
||||
const data2 = ref(25);
|
||||
export default {
|
||||
setup() {
|
||||
return {
|
||||
data,
|
||||
data2
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
:::
|
||||
|
||||
|
||||
::: title input-number属性
|
||||
:::
|
||||
|
||||
::: table
|
||||
|
||||
| 属性 | 描述 | 类型 | 默认值 |
|
||||
| -------- | ------------- | ------ | ------ |
|
||||
| v-model | 值 | `number` | 0 |
|
||||
| name | 原生属性`name` | `number` | 5 |
|
||||
| disabledInput | 禁用输入框输入 | `boolean` | false |
|
||||
| disabled | 禁用操作 | `boolean` | false |
|
||||
| step | 数字增减的步数 | `number` | 1 |
|
||||
| position | 控制按钮显示位置, 目前除了默认值,只有`right`可填 | `string` | -- |
|
||||
| min | 最小可输入的数 | `number` | -- |
|
||||
| max | 最大可输入的数 | `number` | -- |
|
||||
| size | 尺寸, 可选值`md` / `sm` / `xs`| `string` | 默认为最大尺寸 |
|
||||
|
||||
:::
|
||||
|
||||
::: title 事件
|
||||
:::
|
||||
|
||||
::: table
|
||||
|
||||
| 属性 | 描述 | 回调参数 |
|
||||
| -------- | -------- | ------ |
|
||||
| change | 值更改触发事件 | (value: number) |
|
||||
|
||||
:::
|
75
example/docs/zh-CN/components/tooltip.md
Normal file
75
example/docs/zh-CN/components/tooltip.md
Normal file
@ -0,0 +1,75 @@
|
||||
::: title 基础使用
|
||||
:::
|
||||
|
||||
::: demo
|
||||
|
||||
<template>
|
||||
<lay-tooltip content="假装这里有文字提示">
|
||||
<lay-button>tooltip</lay-button>
|
||||
</lay-tooltip>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
:::
|
||||
|
||||
::: title 显示位置
|
||||
:::
|
||||
|
||||
::: demo
|
||||
|
||||
<template>
|
||||
<div style="padding: 100px;max-width:400px;">
|
||||
<div style="text-align: center;">
|
||||
<lay-tooltip content="假装这里有文字提示">
|
||||
<lay-button>上边</lay-button>
|
||||
</lay-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<lay-tooltip content="假装这里有文字提示假装这里有文字提示假装这里有文字提示假装这里有文字提示假装这里有文字提示" position="left">
|
||||
<lay-button style="float:left;">左边</lay-button>
|
||||
</lay-tooltip>
|
||||
<lay-tooltip content="假装这里有文字提示假装这里有文字提示假装这里有文字提示假装这里有文字提示假装这里有文字提示" position="right">
|
||||
<lay-button style="float:right;">右边</lay-button>
|
||||
</lay-tooltip>
|
||||
<div style="clear: both;"></div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<lay-tooltip content="假装这里有文字提示假装这里有文字提示假装这里有文字提示假装这里有文字提示" position="bottom">
|
||||
<lay-button>下边</lay-button>
|
||||
</lay-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
:::
|
||||
|
||||
::: title 浅色主题
|
||||
:::
|
||||
|
||||
::: demo
|
||||
|
||||
<template>
|
||||
<lay-tooltip content="假装这里有文字提示" :is-dark="false">
|
||||
<lay-button>tooltip</lay-button>
|
||||
</lay-tooltip>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
:::
|
||||
|
||||
::: title tooltip属性
|
||||
:::
|
||||
|
||||
::: table
|
||||
|
||||
| 属性 | 描述 | 可选值 |
|
||||
| ----------- | -------- | -------------- |
|
||||
| content | 显示内容 | -- |
|
||||
| position | 显示位置 | `top`(默认值)、`bottom`、`left`、`right` |
|
||||
| isDark | 是否为黑色主题 | `true`(默认值)、`false`(浅色) |
|
||||
|
||||
:::
|
@ -281,6 +281,12 @@ export default {
|
||||
subTitle: 'input',
|
||||
path: '/zh-CN/components/input',
|
||||
},
|
||||
{
|
||||
id: 341,
|
||||
title: '数字输入框',
|
||||
subTitle: 'inputNumber',
|
||||
path: '/zh-CN/components/inputNumber',
|
||||
},
|
||||
{
|
||||
id: 35,
|
||||
title: '文本域',
|
||||
@ -316,6 +322,11 @@ export default {
|
||||
title: '颜色选择器',
|
||||
subTitle: 'colorPicker',
|
||||
path: '/zh-CN/components/colorPicker',
|
||||
}, {
|
||||
id: 41,
|
||||
title: '文字提示',
|
||||
subTitle: 'tooltip',
|
||||
path: '/zh-CN/components/tooltip',
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -238,6 +238,11 @@ const zhCN = [
|
||||
component: () => import('../../docs/zh-CN/components/input.md'),
|
||||
meta: { title: '输入框' },
|
||||
},
|
||||
{
|
||||
path: '/zh-CN/components/inputNumber',
|
||||
component: () => import('../../docs/zh-CN/components/inputNumber.md'),
|
||||
meta: { title: '数字输入框' },
|
||||
},
|
||||
{
|
||||
path: '/zh-CN/components/textarea',
|
||||
component: () => import('../../docs/zh-CN/components/textarea.md'),
|
||||
@ -273,6 +278,11 @@ const zhCN = [
|
||||
component: () => import('../../docs/zh-CN/components/layer.md'),
|
||||
meta: { title: '简介' },
|
||||
},
|
||||
{
|
||||
path: '/zh-CN/components/tooltip',
|
||||
component: () => import('../../docs/zh-CN/components/tooltip.md'),
|
||||
meta: { title: '文字提示' },
|
||||
},
|
||||
{
|
||||
path: '/zh-CN/components/modal',
|
||||
component: () => import('../../docs/zh-CN/components/modal.md'),
|
||||
|
@ -188,6 +188,12 @@ export default {
|
||||
subTitle: 'input',
|
||||
path: '/zh-CN/components/input',
|
||||
},
|
||||
{
|
||||
id: 341,
|
||||
title: '数字输入框',
|
||||
subTitle: 'inputNumber',
|
||||
path: '/zh-CN/components/inputNumber',
|
||||
},
|
||||
{
|
||||
id: 35,
|
||||
title: '文本域',
|
||||
@ -326,6 +332,12 @@ export default {
|
||||
subTitle: 'field',
|
||||
path: '/zh-CN/components/field',
|
||||
},
|
||||
{
|
||||
id: 25,
|
||||
title: '文字提示',
|
||||
subTitle: 'tooltip',
|
||||
path: '/zh-CN/components/tooltip',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -64,6 +64,8 @@ import LaySlider from './module/slider/index'
|
||||
import LayCarousel from './module/carousel/index'
|
||||
import LayCarouselItem from './module/carouselItem/index'
|
||||
import LayColorPicker from './module/colorPicker/index'
|
||||
import LayTooltip from './module/tooltip/index'
|
||||
import LayInputNumber from './module/inputNumber/index'
|
||||
|
||||
const components: Record<string, IDefineComponent> = {
|
||||
LayRadio,
|
||||
@ -123,6 +125,8 @@ const components: Record<string, IDefineComponent> = {
|
||||
LayCarouselItem,
|
||||
LayColorPicker,
|
||||
LayModal,
|
||||
LayTooltip,
|
||||
LayInputNumber,
|
||||
}
|
||||
|
||||
const install = (app: App, options?: InstallOptions): void => {
|
||||
|
@ -20,7 +20,7 @@ export interface LayInputProps {
|
||||
name?: string;
|
||||
type?: string;
|
||||
disabled?: boolean;
|
||||
modelValue?: string;
|
||||
modelValue?: string | number;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
@ -41,4 +41,5 @@ const onFocus = function (event: FocusEvent) {
|
||||
const onBlur = function () {
|
||||
emit("blur");
|
||||
};
|
||||
|
||||
</script>
|
||||
|
120
src/module/inputNumber/index.less
Normal file
120
src/module/inputNumber/index.less
Normal file
@ -0,0 +1,120 @@
|
||||
@border-color: #cacaca;
|
||||
@hover-border-color: #1E9FFF;
|
||||
@lg: 40px;
|
||||
@lg-wdith: 200px;
|
||||
@lg-right: 20px;
|
||||
@md: 32px;
|
||||
@md-wdith: 160px;
|
||||
@md-right: 16px;
|
||||
@sm: 28px;
|
||||
@sm-wdith: 140px;
|
||||
@sm-right: 14px;
|
||||
@xs: 24px;
|
||||
@xs-wdith: 120px;
|
||||
@xs-right: 12px;
|
||||
|
||||
.set-size(@width, @size, @right-size){
|
||||
&{
|
||||
height: @size;
|
||||
width: @width;
|
||||
.layui-input{
|
||||
height: @size;
|
||||
line-height: @size;
|
||||
padding: 0 @size;
|
||||
}
|
||||
.layui-control-btn {
|
||||
width: @size;
|
||||
height: @size;
|
||||
line-height: @size;
|
||||
}
|
||||
&[position=right]{
|
||||
.layui-input{
|
||||
padding: 0 @size 0 0;
|
||||
}
|
||||
.layui-control-btn {
|
||||
height: @right-size;
|
||||
line-height: @right-size;
|
||||
}
|
||||
.layui-subtraction-btn{
|
||||
top: @right-size - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layui-input-number{
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid @border-color;
|
||||
border-color: @border-color;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
.set-size(@lg-wdith, @lg, @lg-right);
|
||||
margin-left: 5px;
|
||||
&:hover {
|
||||
border-color: @hover-border-color;
|
||||
}
|
||||
.layui-input{
|
||||
text-align: center;
|
||||
border: 0;
|
||||
}
|
||||
.layui-control-btn {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: 0;
|
||||
border-color: @border-color;
|
||||
border-style: solid;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
&:hover{
|
||||
color: @hover-border-color;
|
||||
}
|
||||
&.layui-subtraction-btn{
|
||||
border-right-width: 1px;
|
||||
}
|
||||
&.layui-addition-btn{
|
||||
border-left-width: 1px;
|
||||
right: 0;
|
||||
}
|
||||
.layui-icon{
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
/* 谷歌--去掉自带的控制按钮 */
|
||||
input.layui-input::-webkit-outer-spin-button,
|
||||
input.layui-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/* 火狐--去掉自带的控制按钮 */
|
||||
input.layui-input[type="number"]{
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
&[position=right]{
|
||||
.layui-subtraction-btn{
|
||||
right: 0;
|
||||
border-right-width: 0px;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
.layui-addition-btn{
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&[size=md] {
|
||||
.set-size(@md-wdith,@md, @md-right)
|
||||
}
|
||||
|
||||
&[size=sm] {
|
||||
.set-size(@sm-wdith, @sm, @sm-right)
|
||||
}
|
||||
|
||||
&[size=xs] {
|
||||
.set-size(@xs-wdith, @xs, @xs-right)
|
||||
}
|
||||
|
||||
}
|
9
src/module/inputNumber/index.ts
Normal file
9
src/module/inputNumber/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { App } from 'vue'
|
||||
import Component from './index.vue'
|
||||
import type { IDefineComponent } from '../type/index'
|
||||
|
||||
Component.install = (app: App) => {
|
||||
app.component(Component.name || 'LayInputNumber', Component)
|
||||
}
|
||||
|
||||
export default Component as IDefineComponent
|
144
src/module/inputNumber/index.vue
Normal file
144
src/module/inputNumber/index.vue
Normal file
@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="layui-input-number" :position="position" :size="size">
|
||||
<lay-button
|
||||
type="primary"
|
||||
size="gl"
|
||||
@mousedown="longDown(subtraction)"
|
||||
@mouseup="cancelLongDown"
|
||||
@blur="cancelLongDown"
|
||||
:disabled="minControl"
|
||||
class="layui-control-btn layui-subtraction-btn"
|
||||
>
|
||||
<lay-icon :type="position==='right' ? 'layui-icon-down' : 'layui-icon-subtraction'"></lay-icon>
|
||||
</lay-button>
|
||||
<div class="layui-input-number-input">
|
||||
<lay-input
|
||||
v-model="num"
|
||||
:readonly="disabledInput || disabled"
|
||||
type="number"
|
||||
:name="name"
|
||||
@change="inputChange"
|
||||
></lay-input>
|
||||
</div>
|
||||
<lay-button
|
||||
type="primary"
|
||||
size="gl"
|
||||
@mousedown="longDown(addition)"
|
||||
@mouseup="cancelLongDown"
|
||||
@blur="cancelLongDown"
|
||||
:disabled="maxControl"
|
||||
class="layui-control-btn layui-addition-btn"
|
||||
>
|
||||
<lay-icon :type="position==='right' ? 'layui-icon-up' : 'layui-icon-addition'"></lay-icon>
|
||||
</lay-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "LayInputNumber",
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import layButton from "../button/index";
|
||||
import layIcon from "../icon/index";
|
||||
import layInput from "../input/index";
|
||||
import "./index.less";
|
||||
import {
|
||||
defineProps,
|
||||
defineEmits,
|
||||
ref,
|
||||
watch,
|
||||
withDefaults,
|
||||
computed,
|
||||
} from "vue";
|
||||
|
||||
export interface LayInputNumberProps {
|
||||
modelValue?: number;
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
disabledInput?: boolean;
|
||||
step?: number;
|
||||
position?: "right";
|
||||
min?: number;
|
||||
max?: number;
|
||||
size?: "md" | "sm" | "xs";
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<LayInputNumberProps>(), {
|
||||
disabled: false,
|
||||
disabledInput: false,
|
||||
modelValue: 0,
|
||||
step: 1,
|
||||
min: -Infinity,
|
||||
max: Infinity,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "change"]);
|
||||
|
||||
let num = ref(props.modelValue);
|
||||
watch(num, (val) => {
|
||||
if (props.max !== Infinity && val > props.max) {
|
||||
num.value = props.max;
|
||||
return;
|
||||
}
|
||||
if (props.min !== -Infinity && val < props.min) {
|
||||
num.value = props.min;
|
||||
return;
|
||||
}
|
||||
if (isNumber(num.value)) {
|
||||
tempValue.value = Number(num.value);
|
||||
emit("update:modelValue", tempValue.value);
|
||||
emit("change", tempValue.value);
|
||||
}
|
||||
});
|
||||
|
||||
watch(()=>props.modelValue, (val) => {
|
||||
if (val !== num.value) {
|
||||
num.value = props.modelValue;
|
||||
}
|
||||
});
|
||||
const tempValue = ref(0);
|
||||
let timer: any = 0;
|
||||
|
||||
const minControl = computed(
|
||||
() => props.min !== -Infinity && Number(props.min) >= num.value
|
||||
);
|
||||
const maxControl = computed(
|
||||
() => props.max !== Infinity && Number(props.max) <= num.value
|
||||
);
|
||||
|
||||
const addition = function () {
|
||||
num.value += Number(props.step);
|
||||
};
|
||||
|
||||
const subtraction = function () {
|
||||
num.value -= Number(props.step);
|
||||
};
|
||||
|
||||
const longDown = function (fn: Function) {
|
||||
cancelLongDown();
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
timer = setInterval(() => fn.call(timer), 100);
|
||||
fn.call(timer);
|
||||
};
|
||||
|
||||
const cancelLongDown = function () {
|
||||
clearInterval(timer);
|
||||
};
|
||||
|
||||
const inputChange = function () {
|
||||
if (isNumber(num.value)) {
|
||||
tempValue.value = Number(num.value);
|
||||
return;
|
||||
}
|
||||
num.value = tempValue.value;
|
||||
};
|
||||
|
||||
const isNumber = function (num: any) {
|
||||
return /^\d+(\.\d+)?$/.test(num);
|
||||
};
|
||||
</script>
|
50
src/module/popper/calcPosition.ts
Normal file
50
src/module/popper/calcPosition.ts
Normal file
@ -0,0 +1,50 @@
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
return {
|
||||
top: `${top - Math.abs(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;
|
||||
}
|
||||
return {
|
||||
top: `${top - Math.abs(popper.offsetHeight - el.offsetHeight) / 2}px`,
|
||||
left: `${right}px`
|
||||
}
|
||||
}
|
||||
}
|
||||
export default postionFns;
|
100
src/module/popper/index.less
Normal file
100
src/module/popper/index.less
Normal file
@ -0,0 +1,100 @@
|
||||
// 主题颜色
|
||||
// 浅色 --> 默认使用
|
||||
@ligh-background: #FFF;
|
||||
@ligh-color: #3a3a3a;
|
||||
|
||||
// 深色
|
||||
@dark-background: #353535;
|
||||
@dark-color: #FFF;
|
||||
|
||||
@border-clor: #cecece;
|
||||
|
||||
// 单一设置主题
|
||||
.single-theme(@position, @contrary_position, @margin_postion, @color) {
|
||||
@attr : ~'[position=@{position}]';
|
||||
&.layui-popper@{attr}{
|
||||
margin-@{contrary_position}: 6px;
|
||||
.layui-popper-arrow {
|
||||
@{contrary_position}: -6px;
|
||||
border-@{contrary_position}-width: 0;
|
||||
border-@{position}-color: @border-clor;
|
||||
&::after{
|
||||
@{contrary_position}: 1px;
|
||||
border-@{contrary_position}-width: 0;
|
||||
margin-@{margin_postion}: -6px;
|
||||
border-@{position}-color: @color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 统一设置四个方向的主题
|
||||
.theme(@background-color, @color) {
|
||||
background-color: @background-color;
|
||||
color: @color;
|
||||
.single-theme(top, bottom, left, @background-color);
|
||||
.single-theme(bottom, top, left, @background-color);
|
||||
.single-theme(right, left, top, @background-color);
|
||||
.single-theme(left, right, top, @background-color);
|
||||
}
|
||||
|
||||
// 箭头默认居中
|
||||
.arrow-default-center(@position, @prop) {
|
||||
@attr : ~'[position=@{position}]';
|
||||
&.layui-popper@{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);
|
||||
}
|
||||
|
||||
// 样式开始
|
||||
.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;
|
||||
border: 1px solid @border-clor;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.15);
|
||||
.theme(@ligh-background, @ligh-color);
|
||||
max-width: 300px;
|
||||
z-index: 9999;
|
||||
|
||||
// 箭头默认居中
|
||||
.all-arrow-default-center();
|
||||
|
||||
.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);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
}
|
89
src/module/popper/index.vue
Normal file
89
src/module/popper/index.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<transition v-show="visible">
|
||||
<div :class="['layui-popper', {'layui-dark' : isDark}]" :style="style" :position="innnerPosition">
|
||||
<slot>{{content}}</slot>
|
||||
<div class="layui-popper-arrow"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
const NAME = "LayPopper";
|
||||
export default {
|
||||
name: NAME
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import "./index.less";
|
||||
import postionFns from "./calcPosition";
|
||||
import { getCurrentInstance, CSSProperties, ref, watch, onUpdated, defineEmits, onMounted} from "vue";
|
||||
import {on} from "../../tools/domUtil";
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
el : any,
|
||||
content ?: string,
|
||||
position ?: string,
|
||||
trigger ?: string,
|
||||
enterable ?: boolean,
|
||||
isDark ?: boolean,
|
||||
modelValue ?: boolean
|
||||
}>(),
|
||||
{
|
||||
position : 'top',
|
||||
enterable : true,
|
||||
isDark : false,
|
||||
trigger : 'hover',
|
||||
modelValue : true
|
||||
}
|
||||
);
|
||||
|
||||
const EVENT_MAP : any = {
|
||||
'hover' : ['mouseenter', null, 'mouseleave', false],
|
||||
'click' : ['click', document, 'click', true]
|
||||
}
|
||||
|
||||
const triggerArr = EVENT_MAP[props.trigger];
|
||||
if (!triggerArr) {
|
||||
console.error(`${NAME} render error!cause: 'Trigger' must be 'hover/click' `)
|
||||
}
|
||||
|
||||
const style = ref<CSSProperties>({top: (-window.innerHeight) + 'px',left:0});
|
||||
const visible = ref(props.modelValue);
|
||||
const checkTarget = ref(false);
|
||||
const popper = ref();
|
||||
const innnerPosition = ref(props.position);
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
watch(visible, (val)=>{
|
||||
emit('update:modelValue', val);
|
||||
})
|
||||
|
||||
const doShow = function(){
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
const doHidden = function(e : MouseEvent){
|
||||
if ((checkTarget.value && props.el.contains(e.target)) || (props.enterable && popper.value.contains(e.target))) return;
|
||||
style.value = {top: (-window.innerHeight) + 'px',left:0};
|
||||
visible.value = false;
|
||||
innnerPosition.value = props.position;
|
||||
}
|
||||
|
||||
// 事件绑定
|
||||
on(props.el, triggerArr[0], doShow);
|
||||
on(triggerArr[1]??props.el, triggerArr[2], doHidden);
|
||||
checkTarget.value = triggerArr[3];
|
||||
|
||||
// 计算位置显示
|
||||
const showPosistion = function(){
|
||||
postionFns[props.position] && (style.value = postionFns[props.position](props.el, popper.value, innnerPosition));
|
||||
}
|
||||
|
||||
// 节点变化, 获取当前实例对应的dom, 如果为显示状态,则计算位置显示
|
||||
const nodeChange = function(){
|
||||
popper.value = getCurrentInstance()?.vnode.el;
|
||||
visible.value && (popper.value.offsetWidth === 0 ? setTimeout(showPosistion, 0) : showPosistion());
|
||||
}
|
||||
onMounted(nodeChange)
|
||||
onUpdated(nodeChange)
|
||||
</script>
|
26
src/module/popper/usePopper.ts
Normal file
26
src/module/popper/usePopper.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { h, render} from "vue";
|
||||
import popper from "./index.vue";
|
||||
import { once } from "../../tools/domUtil";
|
||||
const EVENT_MAP : any = {
|
||||
'hover': 'mouseenter',
|
||||
'click': 'click'
|
||||
}
|
||||
const usePopper = {
|
||||
createPopper(el: HTMLElement, props: any, trigger : string) {
|
||||
const _this = this;
|
||||
once(el, EVENT_MAP[trigger], () => {
|
||||
const _props = {...props};
|
||||
_props.el = el;
|
||||
_this.renderPopper(_props);
|
||||
})
|
||||
},
|
||||
renderPopper(props: any) {
|
||||
const container: HTMLDivElement = document.createElement("div");
|
||||
// container.setAttribute("class", "lay-div");
|
||||
const node = h(popper, props);
|
||||
render(h(popper, props), container);
|
||||
container.firstElementChild && document.body.appendChild(container.firstElementChild);
|
||||
return node;
|
||||
}
|
||||
}
|
||||
export default usePopper;
|
9
src/module/tooltip/index.ts
Normal file
9
src/module/tooltip/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { App } from 'vue'
|
||||
import Component from './index.vue'
|
||||
import type { IDefineComponent } from '../type/index'
|
||||
|
||||
Component.install = (app: App) => {
|
||||
app.component(Component.name || 'LayTooltip', Component)
|
||||
}
|
||||
|
||||
export default Component as IDefineComponent
|
30
src/module/tooltip/index.vue
Normal file
30
src/module/tooltip/index.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import usePopper from "../popper/usePopper";
|
||||
import { defineComponent} from "vue";
|
||||
export default defineComponent({
|
||||
name: "LayTooltip",
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
position: {
|
||||
type: String,
|
||||
default: "top",
|
||||
},
|
||||
isDark: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return this.$slots.default && this.$slots.default()[0];
|
||||
},
|
||||
mounted() {
|
||||
const _this = this;
|
||||
this.$nextTick(() => {
|
||||
usePopper.createPopper(_this.$el, _this.$props, "hover");
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
27
src/tools/domUtil.js
Normal file
27
src/tools/domUtil.js
Normal file
@ -0,0 +1,27 @@
|
||||
// 获取标签与窗口顶边距离
|
||||
export function getTop(elem) {
|
||||
return elem.offsetTop + (elem.offsetParent && getTop(elem.offsetParent) || 0);
|
||||
}
|
||||
|
||||
// 获取标签与窗口左边距离
|
||||
export function getLeft(elem) {
|
||||
return elem.offsetLeft + (elem.offsetParent && getLeft(elem.offsetParent) || 0);
|
||||
}
|
||||
|
||||
// 事件绑定
|
||||
export function on(elem, events, handler) {
|
||||
[].concat(events).forEach(event => elem.addEventListener(event, handler, false));
|
||||
}
|
||||
|
||||
export function once (elem, events, handler) {
|
||||
const listener = function (_this, args) {
|
||||
handler.apply(_this, args)
|
||||
off(elem, events, listener);
|
||||
}
|
||||
on(elem, events, listener)
|
||||
}
|
||||
|
||||
// 事件解除
|
||||
export function off(elem, events, handler) {
|
||||
[].concat(events).forEach(event => elem.removeEventListener(event, handler, false));
|
||||
}
|
Loading…
Reference in New Issue
Block a user