Browse Source

Merge branch 'next' of https://github.com/jdf2e/nutui into next

yangxiaolu3 4 years ago
parent
commit
68598ea48c

+ 9 - 0
src/config.js

@@ -316,6 +316,15 @@ module.exports = {
         },
         {
           version: '3.0.0',
+          name: 'Range',
+          type: 'component',
+          cName: '区间选择器',
+          desc: '滑动输入条,用于在给定的范围内选择一个值。',
+          sort: 16,
+          show: true,
+          author: 'Jerry'
+        },
+        {
           name: 'PullRefresh',
           type: 'component',
           cName: '下拉刷新',

+ 33 - 0
src/packages/range/demo.vue

@@ -0,0 +1,33 @@
+<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>

+ 34 - 0
src/packages/range/doc.md

@@ -0,0 +1,34 @@
+#  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 |
+    

+ 63 - 0
src/packages/range/index.scss

@@ -0,0 +1,63 @@
+.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;
+    }
+  }
+}

+ 255 - 0
src/packages/range/index.vue

@@ -0,0 +1,255 @@
+<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>

+ 2 - 0
src/sites/mobile/main.ts

@@ -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)

+ 197 - 0
src/utils/touchEmulator.js

@@ -0,0 +1,197 @@
+/* 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();
+}

+ 48 - 0
src/utils/useRect/index.ts

@@ -0,0 +1,48 @@
+/**
+  获取元素的大小及其相对于视口的位置,等价于 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
+  };
+};

+ 69 - 0
src/utils/useTouch/index.ts

@@ -0,0 +1,69 @@
+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
+  };
+}