<template>
<div class="ion-range"
:class="[modeClass,colorClass,{'range-pressed':pressed},{'range-disabled':disabled},{'range-has-pin':pin}]">
<slot name="range-left"></slot>
<div ref="slider" class="range-slider">
<div class="range-tick" v-for="t in ticks" :style="{ left: t.left }"
:class="{'range-tick-active':t.active}"
role="presentation"></div>
<div class="range-bar" role="presentation"></div>
<div class="range-bar range-bar-active" :style="{ left: barL,right:barR }" ref="bar"
role="presentation"></div>
<RangeKnobHandle
:ratio="ratioA"
:val="valA"
:pin="pin"
:pressed="pressedA"
:min="min"
:max="max"
:disabled="disabled">
</RangeKnobHandle>
<RangeKnobHandle
:ratio="ratioB"
:val="valB"
:pin="pin"
:pressed="pressedB"
:min="min"
:max="max"
:disabled="disabled"
v-if="dualKnobs">
</RangeKnobHandle>
</div>
<slot name="range-right"></slot>
</div>
</template>
<style lang="scss" src="./style.scss"></style>
<script type="text/javascript">
/**
*
* @component Range
* @description
*
* ## 表单组件 / Range滑块组件
*
*
* ### 注意
*
* @props {String} [color] - 颜色
* @props {Boolean} [disabled=false] - 是否禁用
* @props {Boolean} [dualKnobs=false] - 选择的拖动按钮, 默认是一个, true为两个
* @props {Number} [max=100] - range的最大值
* @props {Number} [min=0] - range的最小值
* @props {String} [mode='ios'] - 模式
* @props {Boolean} [pin=false] - 当拖动knob时显示大头针提示
* @props {Boolean} [snaps=false] - 类似于卡槽, 如果为true, 则在range上画标尺, 并且拖动中knob只能停留在标尺标记处
* @props {Number} [step=1] - 移动的步伐/粒度
* @props {String| Number| Object} [value] - v-model对应的值, 需要出发input事件
*
* @slot range-right - 在range组件右边, 一般放Icon
* @slot range-left - 在range组件左边, 一般放Icon
*
* @demo #/range
*
* @usage
* <List>
* <ListHeader>
* <span>Brightness</span>
* <Badge slot="item-right">{{brightness}}</Badge>
* </ListHeader>
* <Item>
* <Range v-model="brightness">
* <Icon slot="range-left" small name="sunny"></Icon>
* <Icon slot="range-right" name="sunny"></Icon>
* </Range>
* </Item>
* </List>
*
* */
import RangeKnobHandle from './range-knob-handle.vue';
import { clamp, pointerCoord, setElementClass } from '../../util/util';
import { isNumber, isObject, isString } from '../../util/type';
import throttle from 'lodash.throttle';
import PointerEvents from './pointer-events.es5.js';
export default {
name: 'Range',
inject: {
itemComponent: {
from: 'itemComponent',
default: null
}
},
data() {
return {
ticks: [], // 移动的标尺
pressed: false, // 拖动knob
moving: false, //
barL: 0, // 左边bar的位置
barR: 0, // 右边bar的位置
ratioA: 0, // 左边bar的比例
ratioB: 0, // 右边bar的比例
valA: 0,
valB: 0,
pressedA: false,
pressedB: false,
_sliderEl: null, // slider的DOM句柄
_rect: null, // _sliderEl的尺寸对象
_activeB: null, // 是否激活了B按钮
_timer: 0,
pointerEvents: null,
valueInner: JSON.parse(JSON.stringify(this.value)) // value内部值
};
},
props: {
/**
* 颜色: "primary", "secondary", "danger", "light", and "dark"
*/
color: String,
/**
* 是否禁用
* */
disabled: Boolean,
/**
* 选择的拖动按钮, 默认是一个, true为两个
* */
dualKnobs: Boolean,
/**
* range的最大值
* */
max: {
type: Number,
default: 100
},
/**
* range的最小值
* */
min: {
type: Number,
default: 0
},
mode: {
type: String,
default() {
return this.$config && this.$config.get('mode', 'ios') || 'ios';
}
},
/**
* 当拖动knob时显示大头针提示
* */
pin: Boolean,
/**
* 类似于卡槽, 如果为true, 则在range上画标尺,
* 并且拖动中knob只能停留在标尺标记处
* */
snaps: Boolean,
/**
* 移动的步伐/粒度
* */
step: {
type: Number,
default: 1
},
/**
* v-model对应的值,
* 需要出发input事件
*/
value: [String, Number, Object]
},
computed: {
// 环境样式
modeClass() {
return `range range-${this.mode}`;
},
// 颜色
colorClass() {
return this.color ? (`range-${this.mode}-${this.color}`) : '';
},
/**
* 返回knob现在的位置比, ratio数值在0/1之间,
* 如果使用了两个knob, 则返回最小那个
*/
ratio() {
if (this.dualKnobs) {
return Math.min(this.ratioA, this.ratioB);
}
return this.ratioA;
},
/**
* 在有两个knob的情况下, 返回最大的按个knob的ratio
*/
ratioUpper() {
if (this.dualKnobs) {
return Math.max(this.ratioA, this.ratioB);
}
return 0;
}
},
methods: {
mouseOutHandler() {
this.moving = false;
},
/**
* 拖动开始
* @param {UIEvent} ev
* @return {boolean}
* */
pointerDown(ev) {
if (this.disabled) {
return false;
}
this.moving = true;
// prevent default so scrolling does not happen
ev.preventDefault();
ev.stopPropagation();
// get the start coordinates
const current = pointerCoord(ev);
// 获取slider元素的尺寸
const rect = this._rect = this._sliderEl.getBoundingClientRect();
// 判断点击的位置离那个knob近
const ratio = clamp(0, (current.x - rect.left) / (rect.width), 1);
this._activeB = (Math.abs(ratio - this.ratioA) > Math.abs(ratio - this.ratioB));
// 更新激活状态的knob位置
this.updateRange(current, rect, true);
// return true so the pointer events
// know everything's still valid
return true;
},
/**
* 拖动处理
* @param {UIEvent} ev
* */
pointerMove(ev) {
if (!this.disabled) {
if (!this.moving) return;
// prevent default so scrolling does not happen
ev.preventDefault();
ev.stopPropagation();
const current = pointerCoord(ev);
// update the active knob's position
this.updateRange(current, this._rect, true);
// 告知v-model
this.$_emit();
}
},
$_emit: throttle(function () {
// 频发触发会导致卡顿
this.$emit('input', JSON.parse(JSON.stringify(this.valueInner)));
}, 16 * 3),
/**
* 拖动停止
* @param {UIEvent} ev
* */
pointerUp(ev) {
this.moving = false;
if (!this.disabled) {
// prevent default so scrolling does not happen
ev.preventDefault();
ev.stopPropagation();
// update the active knob's position
this.updateRange(pointerCoord(ev), this._rect, false);
}
},
/**
* 更新knob的位置
* @param {PointerCoordinates} current
* @param {ClientRect} rect
* @param {boolean} isPressed
*/
updateRange(current, rect, isPressed) {
// figure out where the pointer is currently at
// update the knob being interacted with
let ratio = clamp(0, (current.x - rect.left) / (rect.width), 1);
let val = this.ratioToValue(ratio);
if (this.snaps) {
// snaps the ratio to the current value
ratio = this.valueToRatio(val);
}
// update which knob is pressed
this.pressed = isPressed;
if (this._activeB) {
// when the pointer down started it was determined
// that knob B was the one they were interacting with
this.pressedB = isPressed;
this.pressedA = false;
this.ratioB = ratio;
if (val === this.valB) {
// hasn't changed
return false;
}
this.valB = val;
} else {
// interacting with knob A
this.pressedA = isPressed;
this.pressedB = false;
this.ratioA = ratio;
if (val === this.valA) {
// hasn't changed
return false;
}
this.valA = val;
}
// value has been updated
if (this.dualKnobs) {
// dual knobs have an lower and upper value
if (!this.valueInner) {
// ensure we're always updating the same object
this.valueInner = {};
}
this.valueInner.lower = Math.min(this.valA, this.valB);
this.valueInner.upper = Math.max(this.valA, this.valB);
} else {
// single knob only has one value
this.valueInner = this.valA;
}
this.updateBar();
return true;
},
/**
* @param {number} ratio
*/
ratioToValue(ratio) {
ratio = Math.round(((this.max - this.min) * ratio));
ratio = Math.round(ratio / this.step) * this.step + this.min;
return clamp(this.min, ratio, this.max);
},
/**
* @param {number} value
*/
valueToRatio(value) {
value = Math.round((value - this.min) / this.step) * this.step;
value = value / (this.max - this.min);
return clamp(0, value, 1);
},
/**
* 更新bar的位置
*/
updateBar() {
const ratioA = this.ratioA;
const ratioB = this.ratioB;
if (this.dualKnobs) {
this.barL = `${(Math.min(ratioA, ratioB) * 100)}%`;
this.barR = `${100 - (Math.max(ratioA, ratioB) * 100)}%`;
} else {
this.barL = '';
this.barR = `${100 - (ratioA * 100)}%`;
}
this.updateTicks();
},
/**
* 更新bar的位置
* 更新标尺
*/
updateTicks() {
const ticks = this.ticks;
const ratio = this.ratio;
if (this.snaps && ticks) {
if (this.dualKnobs) {
var upperRatio = this.ratioUpper;
ticks.forEach(t => {
t.active = (t.ratio >= ratio && t.ratio <= upperRatio);
});
} else {
ticks.forEach(t => {
t.active = (t.ratio <= ratio);
});
}
}
},
/**
* 在rangebar上画上移动的标尺
* */
createTicks() {
if (this.snaps) {
this.$nextTick(function () {
this.ticks = [];
for (var value = this.min; value <= this.max; value += this.step) {
var ratio = this.valueToRatio(value);
this.ticks.push({
ratio: ratio,
left: `${ratio * 100}%`
});
}
this.updateTicks();
});
}
},
/**
* init
* 为父元素item设置class需要等待mounted之后
*/
initDOM() {
// 在item父元素上添加类item-range
if (this.itemComponent) {
if (this.itemComponent.$el) {
setElementClass(this.itemComponent.$el, 'item-range', true);
// setElementClass(this.itemComponent.$el, 'item-range-disabled', this.disabled);
}
}
// 获取slider的DOM
this._sliderEl = this.$refs.slider;
// 为range左右添加属性
if (this.$slots && this.$slots['range-left']) {
this.$slots['range-left'].forEach(function (item) {
item.elm.setAttribute('range-left', '');
});
}
if (this.$slots && this.$slots['range-right']) {
this.$slots['range-right'].forEach(function (item) {
item.elm.setAttribute('range-right', '');
});
}
this.pointerEvents = new PointerEvents(this._sliderEl, this.pointerDown, this.pointerMove, this.pointerUp, {});
},
/**
* init
* 初始化传入数据并更新视图
*/
initData() {
// 设置标尺
this.createTicks();
// 设置value的初始状态
if (isString(this.valueInner)) {
let val = Math.round(this.valueInner);
if (!isNaN(val)) {
this.valueInner = val;
} else {
this.valueInner = 0;
}
}
if (isNumber(this.valueInner)) {
this.ratioA = this.valueToRatio(this.valueInner);
}
if (isObject(this.valueInner)) {
this.valA = Math.min(this.valueInner.lower, this.valueInner.upper);
this.valB = Math.max(this.valueInner.lower, this.valueInner.upper);
this.ratioA = this.valueToRatio(this.valA);
this.ratioB = this.valueToRatio(this.valB);
}
// 更新bar
this.updateBar();
}
},
created() {
this.initData();
},
mounted() {
this.initDOM();
},
destroy() {
this.pointerEvents && this.pointerEvents.destroy();
},
components: {
RangeKnobHandle: RangeKnobHandle
}
};
</script>