提交 490223d5 编写于 作者: Z zy19940510

feat: 初始化range组件

上级 13fac072
......@@ -313,6 +313,16 @@ module.exports = {
sort: 15,
show: true,
author: 'yangxiaolu'
},
{
version: '3.0.0',
name: 'Range',
type: 'component',
cName: '区间选择器',
desc: '滑动输入条,用于在给定的范围内选择一个值。',
sort: 16,
show: true,
author: 'Jerry'
}
]
},
......
<template>
<div class="demo">
<h2>基础用法</h2>
<nut-cell class="cell">
<nut-range v-model="value" @change="onChange"></nut-range>
</nut-cell>
</div>
</template>
<script lang="ts">
import { ref } from 'vue';
import { createComponent } from '@/utils/create';
const { createDemo } = createComponent('range');
export default createDemo({
props: {},
setup() {
const value = ref(50);
const onChange = value => console.log('当前值:' + value);
return {
value,
onChange
};
}
});
</script>
<style lang="scss" scoped>
.cell {
padding: 30px 18px;
}
.nut-range {
}
</style>
# range组件
### 介绍
基于 xxxxxxx
### 安装
## 代码演示
### 基础用法1
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
|--------------|----------------------------------|--------|------------------|
| name | 图标名称或图片链接 | String | - |
| color | 图标颜色 | String | - |
| size | 图标大小,如 '20px' '2em' '2rem' | String | - |
| class-prefix | 类名前缀,用于使用自定义图标 | String | 'nutui-iconfont' |
| tag | HTML 标签 | String | 'i' |
### Events
| 事件名 | 说明 | 回调参数 |
|--------|----------------|--------------|
| click | 点击图标时触发 | event: Event |
\ No newline at end of file
.nut-range {
display: block;
position: relative;
width: 100%;
height: 3px;
background-color: rgba(255, 163, 154, 1);
border-radius: 2px;
cursor: pointer;
&::before {
position: absolute;
top: -8px;
right: 0;
bottom: -8px;
left: 0;
content: '';
}
&-bar {
display: block;
position: relative;
width: 100%;
height: 100%;
background: linear-gradient(
135deg,
rgba(250, 44, 25, 1) 0%,
rgba(250, 63, 25, 1) 45%,
rgba(250, 89, 25, 1) 83%,
rgba(250, 100, 25, 1) 100%
);
border-radius: inherit;
transition: all 0.2s;
}
&-button {
display: block;
width: 24px;
height: 24px;
background-color: #fff;
border-radius: 50%;
box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.15);
border: 1px solid rgba(250, 44, 25, 1);
outline: none;
&-wrapper,
&-wrapper-right {
position: absolute;
top: 50%;
right: 0;
transform: translate3d(50%, -50%, 0);
cursor: grab;
outline: none;
}
&-wrapper-left {
position: absolute;
top: 50%;
left: 0;
transform: translate3d(-50%, -50%, 0);
cursor: grab;
outline: none;
}
}
}
<template>
<view ref="root" :style="wrapperStyle" :class="classes" @click.stop="onClick">
<view class="nut-range-bar" :style="barStyle">
<view
role="slider"
class="nut-range-button-wrapper"
:tabindex="disabled ? -1 : 0"
:aria-valuemin="+min"
:aria-valuenow="curValue()"
:aria-valuemax="+max"
aria-orientation="horizontal"
@touchstart.stop.prevent="onTouchStart"
@touchmove.stop.prevent="onTouchMove"
@touchend.stop.prevent="onTouchEnd"
@touchcancel.stop.prevent="onTouchEnd"
@click="e => e.stopPropagation()"
>
<slot v-if="$slots.button"></slot>
<view class="nut-range-button" v-else></view>
</view>
</view>
</view>
</template>
<script lang="ts">
import { ref, toRefs, computed, PropType, CSSProperties } from 'vue';
import { createComponent } from '@/utils/create';
import { useTouch } from '@/utils/useTouch';
import { useRect } from '@/utils/useRect';
const { componentName, create } = createComponent('range');
type SliderValue = number | number[];
export default create({
props: {
range: {
type: Boolean,
default: false
},
disabled: Boolean,
barHeight: [Number, String],
activeColor: String,
inactiveColor: String,
min: {
type: [Number, String],
default: 0
},
max: {
type: [Number, String],
default: 100
},
step: {
type: [Number, String],
default: 1
},
modelValue: {
type: [Number, Array] as PropType<SliderValue>,
default: 0
}
},
components: {},
emits: ['change', 'drag-end', 'drag-start', 'update:modelValue'],
setup(props, { emit }) {
let buttonIndex: number;
let startValue: SliderValue;
let currentValue: SliderValue;
const root = ref<HTMLElement>();
const dragStatus = ref<'start' | 'draging' | ''>();
const touch = useTouch();
const scope = computed(() => Number(props.max) - Number(props.min));
const classes = computed(() => {
const prefixCls = componentName;
return {
[prefixCls]: true,
[`${prefixCls}-disabled`]: props.disabled
};
});
const wrapperStyle = computed(() => {
return {
background: props.inactiveColor
};
});
const isRange = (val: unknown): val is number[] =>
!!props.range && Array.isArray(val);
// 计算选中条的长度百分比
const calcMainAxis = () => {
const { modelValue, min } = props;
if (isRange(modelValue)) {
return `${((modelValue[1] - modelValue[0]) * 100) / scope.value}%`;
}
return `${((modelValue - Number(min)) * 100) / scope.value}%`;
};
// 计算选中条的开始位置的偏移量
const calcOffset = () => {
const { modelValue, min } = props;
if (isRange(modelValue)) {
return `${((modelValue[0] - Number(min)) * 100) / scope.value}%`;
}
return `0%`;
};
const barStyle = computed<CSSProperties>(() => {
return {
width: calcMainAxis(),
left: calcOffset(),
background: props.activeColor,
transition: dragStatus.value ? 'none' : undefined
};
});
const format = (value: number) => {
const { min, max, step } = props;
value = Math.max(+min, Math.min(value, +max));
return Math.round(value / +step) * +step;
};
const isSameValue = (newValue: SliderValue, oldValue: SliderValue) =>
JSON.stringify(newValue) === JSON.stringify(oldValue);
// 处理两个滑块重叠之后的情况
const handleOverlap = (value: number[]) => {
if (value[0] > value[1]) {
return value.slice(0).reverse();
}
return value;
};
const updateValue = (value: SliderValue, end?: boolean) => {
if (isRange(value)) {
value = handleOverlap(value).map(format);
} else {
value = format(value);
}
if (!isSameValue(value, props.modelValue)) {
emit('update:modelValue', value);
}
if (end && !isSameValue(value, startValue)) {
emit('change', value);
}
};
const onClick = (event: MouseEvent) => {
if (props.disabled) {
return;
}
const { min, modelValue } = props;
const rect = useRect(root);
const delta = event.clientX - rect.left;
const total = rect.width;
const value = Number(min) + (delta / total) * scope.value;
if (isRange(modelValue)) {
const [left, right] = modelValue;
const middle = (left + right) / 2;
if (value <= middle) {
updateValue([value, right], true);
} else {
updateValue([left, value], true);
}
} else {
updateValue(value, true);
}
};
const onTouchStart = (event: TouchEvent) => {
if (props.disabled) {
return;
}
touch.start(event);
currentValue = props.modelValue;
if (isRange(currentValue)) {
startValue = currentValue.map(format);
} else {
startValue = format(currentValue);
}
dragStatus.value = 'start';
};
const onTouchMove = (event: TouchEvent) => {
if (props.disabled) {
return;
}
if (dragStatus.value === 'start') {
emit('drag-start');
}
touch.move(event);
dragStatus.value = 'draging';
const rect = useRect(root);
const delta = touch.deltaX.value;
const total = rect.width;
const diff = (delta / total) * scope.value;
if (isRange(startValue)) {
(currentValue as number[])[buttonIndex] =
startValue[buttonIndex] + diff;
} else {
currentValue = startValue + diff;
}
updateValue(currentValue);
};
const onTouchEnd = () => {
if (props.disabled) {
return;
}
if (dragStatus.value === 'draging') {
updateValue(currentValue, true);
emit('drag-end');
}
dragStatus.value = '';
};
const curValue = (idx?) => {
const value =
typeof idx === 'number'
? (props.modelValue as number[])[idx]
: (props.modelValue as number);
return value;
};
return {
root,
classes,
wrapperStyle,
onClick,
onTouchStart,
onTouchMove,
onTouchEnd,
...toRefs(props),
barStyle,
curValue
};
}
});
</script>
<style lang="scss">
@import 'index.scss';
</style>
......@@ -3,6 +3,8 @@ import App from './App.vue';
import router from './router';
import NutUI from '@/nutui';
import '@/sites/assets/styles/reset.scss';
import '@/utils/touchEmulator';
createApp(App)
.use(router)
.use(NutUI)
......
/* eslint-disable */
/**
* Emulate touch event
* Source:https://github.com/hammerjs/touchemulator
*/
var eventTarget;
var supportTouch = 'ontouchstart' in window;
// polyfills
if (!document.createTouch) {
document.createTouch = function(
view,
target,
identifier,
pageX,
pageY,
screenX,
screenY
) {
// auto set
return new Touch(
target,
identifier,
{
pageX: pageX,
pageY: pageY,
screenX: screenX,
screenY: screenY,
clientX: pageX - window.pageXOffset,
clientY: pageY - window.pageYOffset
},
0,
0
);
};
}
if (!document.createTouchList) {
document.createTouchList = function() {
var touchList = TouchList();
for (var i = 0; i < arguments.length; i++) {
touchList[i] = arguments[i];
}
touchList.length = arguments.length;
return touchList;
};
}
/**
* create an touch point
* @constructor
* @param target
* @param identifier
* @param pos
* @param deltaX
* @param deltaY
* @returns {Object} touchPoint
*/
var Touch = function Touch(target, identifier, pos, deltaX, deltaY) {
deltaX = deltaX || 0;
deltaY = deltaY || 0;
this.identifier = identifier;
this.target = target;
this.clientX = pos.clientX + deltaX;
this.clientY = pos.clientY + deltaY;
this.screenX = pos.screenX + deltaX;
this.screenY = pos.screenY + deltaY;
this.pageX = pos.pageX + deltaX;
this.pageY = pos.pageY + deltaY;
};
/**
* create empty touchlist with the methods
* @constructor
* @returns touchList
*/
function TouchList() {
var touchList = [];
touchList['item'] = function(index) {
return this[index] || null;
};
// specified by Mozilla
touchList['identifiedTouch'] = function(id) {
return this[id + 1] || null;
};
return touchList;
}
/**
* only trigger touches when the left mousebutton has been pressed
* @param touchType
* @returns {Function}
*/
var initiated = false;
function onMouse(touchType) {
return function(ev) {
// prevent mouse events
if (ev.type === 'mousedown') {
initiated = true;
}
if (ev.type === 'mouseup') {
initiated = false;
}
if (ev.type === 'mousemove' && !initiated) {
return;
}
// The EventTarget on which the touch point started when it was first placed on the surface,
// even if the touch point has since moved outside the interactive area of that element.
// also, when the target doesnt exist anymore, we update it
if (
ev.type === 'mousedown' ||
!eventTarget ||
(eventTarget && !eventTarget.dispatchEvent)
) {
eventTarget = ev.target;
}
triggerTouch(touchType, ev);
// reset
if (ev.type === 'mouseup') {
eventTarget = null;
}
};
}
/**
* trigger a touch event
* @param eventName
* @param mouseEv
*/
function triggerTouch(eventName, mouseEv) {
var touchEvent = document.createEvent('Event');
touchEvent.initEvent(eventName, true, true);
touchEvent.altKey = mouseEv.altKey;
touchEvent.ctrlKey = mouseEv.ctrlKey;
touchEvent.metaKey = mouseEv.metaKey;
touchEvent.shiftKey = mouseEv.shiftKey;
touchEvent.touches = getActiveTouches(mouseEv);
touchEvent.targetTouches = getActiveTouches(mouseEv);
touchEvent.changedTouches = createTouchList(mouseEv);
eventTarget.dispatchEvent(touchEvent);
}
/**
* create a touchList based on the mouse event
* @param mouseEv
* @returns {TouchList}
*/
function createTouchList(mouseEv) {
var touchList = TouchList();
touchList.push(new Touch(eventTarget, 1, mouseEv, 0, 0));
return touchList;
}
/**
* receive all active touches
* @param mouseEv
* @returns {TouchList}
*/
function getActiveTouches(mouseEv) {
// empty list
if (mouseEv.type === 'mouseup') {
return TouchList();
}
return createTouchList(mouseEv);
}
/**
* TouchEmulator initializer
*/
function TouchEmulator() {
window.addEventListener('mousedown', onMouse('touchstart'), true);
window.addEventListener('mousemove', onMouse('touchmove'), true);
window.addEventListener('mouseup', onMouse('touchend'), true);
}
// start distance when entering the multitouch mode
TouchEmulator['multiTouchOffset'] = 75;
if (!supportTouch) {
new TouchEmulator();
}
/**
获取元素的大小及其相对于视口的位置,等价于 Element.getBoundingClientRect。
width 宽度 number
height 高度 number
top 顶部与视图窗口左上角的距离 number
left 左侧与视图窗口左上角的距离 number
right 右侧与视图窗口左上角的距离 number
bottom 底部与视图窗口左上角的距离 number
*/
import { Ref, unref } from 'vue';
function isWindow(val: unknown): val is Window {
return val === window;
}
export const useRect = (
elementRef: (Element | Window) | Ref<Element | Window | undefined>
) => {
const element = unref(elementRef);
if (isWindow(element)) {
const width = element.innerWidth;
const height = element.innerHeight;
return {
top: 0,
left: 0,
right: width,
bottom: height,
width,
height
};
}
if (element && element.getBoundingClientRect) {
return element.getBoundingClientRect();
}
return {
top: 0,
left: 0,
right: 0,
bottom: 0,
width: 0,
height: 0
};
};
import { ref } from 'vue';
const MIN_DISTANCE = 10;
type Direction = '' | 'vertical' | 'horizontal';
function getDirection(x: number, y: number) {
if (x > y && x > MIN_DISTANCE) {
return 'horizontal';
}
if (y > x && y > MIN_DISTANCE) {
return 'vertical';
}
return '';
}
export function useTouch() {
const startX = ref(0);
const startY = ref(0);
const deltaX = ref(0);
const deltaY = ref(0);
const offsetX = ref(0);
const offsetY = ref(0);
const direction = ref<Direction>('');
const isVertical = () => direction.value === 'vertical';
const isHorizontal = () => direction.value === 'horizontal';
const reset = () => {
deltaX.value = 0;
deltaY.value = 0;
offsetX.value = 0;
offsetY.value = 0;
direction.value = '';
};
const start = ((event: TouchEvent) => {
reset();
startX.value = event.touches[0].clientX;
startY.value = event.touches[0].clientY;
}) as EventListener;
const move = ((event: TouchEvent) => {
const touch = event.touches[0];
deltaX.value = touch.clientX - startX.value;
deltaY.value = touch.clientY - startY.value;
offsetX.value = Math.abs(deltaX.value);
offsetY.value = Math.abs(deltaY.value);
if (!direction.value) {
direction.value = getDirection(offsetX.value, offsetY.value);
}
}) as EventListener;
return {
move,
start,
reset,
startX,
startY,
deltaX,
deltaY,
offsetX,
offsetY,
direction,
isVertical,
isHorizontal
};
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册