first
This commit is contained in:
8
packages/slider/index.js
Normal file
8
packages/slider/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import Slider from './src/main';
|
||||
|
||||
/* istanbul ignore next */
|
||||
Slider.install = function(Vue) {
|
||||
Vue.component(Slider.name, Slider);
|
||||
};
|
||||
|
||||
export default Slider;
|
238
packages/slider/src/button.vue
Normal file
238
packages/slider/src/button.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div
|
||||
class="el-slider__button-wrapper"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@mousedown="onButtonDown"
|
||||
@touchstart="onButtonDown"
|
||||
:class="{ 'hover': hovering, 'dragging': dragging }"
|
||||
:style="wrapperStyle"
|
||||
ref="button"
|
||||
tabindex="0"
|
||||
@focus="handleMouseEnter"
|
||||
@blur="handleMouseLeave"
|
||||
@keydown.left="onLeftKeyDown"
|
||||
@keydown.right="onRightKeyDown"
|
||||
@keydown.down.prevent="onLeftKeyDown"
|
||||
@keydown.up.prevent="onRightKeyDown"
|
||||
>
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
ref="tooltip"
|
||||
:popper-class="tooltipClass"
|
||||
:disabled="!showTooltip">
|
||||
<span slot="content">{{ formatValue }}</span>
|
||||
<div class="el-slider__button" :class="{ 'hover': hovering, 'dragging': dragging }"></div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ElTooltip from 'element-ui/packages/tooltip';
|
||||
|
||||
export default {
|
||||
name: 'ElSliderButton',
|
||||
|
||||
components: {
|
||||
ElTooltip
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
tooltipClass: String
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
hovering: false,
|
||||
dragging: false,
|
||||
isClick: false,
|
||||
startX: 0,
|
||||
currentX: 0,
|
||||
startY: 0,
|
||||
currentY: 0,
|
||||
startPosition: 0,
|
||||
newPosition: null,
|
||||
oldValue: this.value
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
disabled() {
|
||||
return this.$parent.sliderDisabled;
|
||||
},
|
||||
|
||||
max() {
|
||||
return this.$parent.max;
|
||||
},
|
||||
|
||||
min() {
|
||||
return this.$parent.min;
|
||||
},
|
||||
|
||||
step() {
|
||||
return this.$parent.step;
|
||||
},
|
||||
|
||||
showTooltip() {
|
||||
return this.$parent.showTooltip;
|
||||
},
|
||||
|
||||
precision() {
|
||||
return this.$parent.precision;
|
||||
},
|
||||
|
||||
currentPosition() {
|
||||
return `${ (this.value - this.min) / (this.max - this.min) * 100 }%`;
|
||||
},
|
||||
|
||||
enableFormat() {
|
||||
return this.$parent.formatTooltip instanceof Function;
|
||||
},
|
||||
|
||||
formatValue() {
|
||||
return this.enableFormat && this.$parent.formatTooltip(this.value) || this.value;
|
||||
},
|
||||
|
||||
wrapperStyle() {
|
||||
return this.vertical ? { bottom: this.currentPosition } : { left: this.currentPosition };
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
dragging(val) {
|
||||
this.$parent.dragging = val;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
displayTooltip() {
|
||||
this.$refs.tooltip && (this.$refs.tooltip.showPopper = true);
|
||||
},
|
||||
|
||||
hideTooltip() {
|
||||
this.$refs.tooltip && (this.$refs.tooltip.showPopper = false);
|
||||
},
|
||||
|
||||
handleMouseEnter() {
|
||||
this.hovering = true;
|
||||
this.displayTooltip();
|
||||
},
|
||||
|
||||
handleMouseLeave() {
|
||||
this.hovering = false;
|
||||
this.hideTooltip();
|
||||
},
|
||||
|
||||
onButtonDown(event) {
|
||||
if (this.disabled) return;
|
||||
event.preventDefault();
|
||||
this.onDragStart(event);
|
||||
window.addEventListener('mousemove', this.onDragging);
|
||||
window.addEventListener('touchmove', this.onDragging);
|
||||
window.addEventListener('mouseup', this.onDragEnd);
|
||||
window.addEventListener('touchend', this.onDragEnd);
|
||||
window.addEventListener('contextmenu', this.onDragEnd);
|
||||
},
|
||||
onLeftKeyDown() {
|
||||
if (this.disabled) return;
|
||||
this.newPosition = parseFloat(this.currentPosition) - this.step / (this.max - this.min) * 100;
|
||||
this.setPosition(this.newPosition);
|
||||
this.$parent.emitChange();
|
||||
},
|
||||
onRightKeyDown() {
|
||||
if (this.disabled) return;
|
||||
this.newPosition = parseFloat(this.currentPosition) + this.step / (this.max - this.min) * 100;
|
||||
this.setPosition(this.newPosition);
|
||||
this.$parent.emitChange();
|
||||
},
|
||||
onDragStart(event) {
|
||||
this.dragging = true;
|
||||
this.isClick = true;
|
||||
if (event.type === 'touchstart') {
|
||||
event.clientY = event.touches[0].clientY;
|
||||
event.clientX = event.touches[0].clientX;
|
||||
}
|
||||
if (this.vertical) {
|
||||
this.startY = event.clientY;
|
||||
} else {
|
||||
this.startX = event.clientX;
|
||||
}
|
||||
this.startPosition = parseFloat(this.currentPosition);
|
||||
this.newPosition = this.startPosition;
|
||||
},
|
||||
|
||||
onDragging(event) {
|
||||
if (this.dragging) {
|
||||
this.isClick = false;
|
||||
this.displayTooltip();
|
||||
this.$parent.resetSize();
|
||||
let diff = 0;
|
||||
if (event.type === 'touchmove') {
|
||||
event.clientY = event.touches[0].clientY;
|
||||
event.clientX = event.touches[0].clientX;
|
||||
}
|
||||
if (this.vertical) {
|
||||
this.currentY = event.clientY;
|
||||
diff = (this.startY - this.currentY) / this.$parent.sliderSize * 100;
|
||||
} else {
|
||||
this.currentX = event.clientX;
|
||||
diff = (this.currentX - this.startX) / this.$parent.sliderSize * 100;
|
||||
}
|
||||
this.newPosition = this.startPosition + diff;
|
||||
this.setPosition(this.newPosition);
|
||||
}
|
||||
},
|
||||
|
||||
onDragEnd() {
|
||||
if (this.dragging) {
|
||||
/*
|
||||
* 防止在 mouseup 后立即触发 click,导致滑块有几率产生一小段位移
|
||||
* 不使用 preventDefault 是因为 mouseup 和 click 没有注册在同一个 DOM 上
|
||||
*/
|
||||
setTimeout(() => {
|
||||
this.dragging = false;
|
||||
this.hideTooltip();
|
||||
if (!this.isClick) {
|
||||
this.setPosition(this.newPosition);
|
||||
this.$parent.emitChange();
|
||||
}
|
||||
}, 0);
|
||||
window.removeEventListener('mousemove', this.onDragging);
|
||||
window.removeEventListener('touchmove', this.onDragging);
|
||||
window.removeEventListener('mouseup', this.onDragEnd);
|
||||
window.removeEventListener('touchend', this.onDragEnd);
|
||||
window.removeEventListener('contextmenu', this.onDragEnd);
|
||||
}
|
||||
},
|
||||
|
||||
setPosition(newPosition) {
|
||||
if (newPosition === null || isNaN(newPosition)) return;
|
||||
if (newPosition < 0) {
|
||||
newPosition = 0;
|
||||
} else if (newPosition > 100) {
|
||||
newPosition = 100;
|
||||
}
|
||||
const lengthPerStep = 100 / ((this.max - this.min) / this.step);
|
||||
const steps = Math.round(newPosition / lengthPerStep);
|
||||
let value = steps * lengthPerStep * (this.max - this.min) * 0.01 + this.min;
|
||||
value = parseFloat(value.toFixed(this.precision));
|
||||
this.$emit('input', value);
|
||||
this.$nextTick(() => {
|
||||
this.displayTooltip();
|
||||
this.$refs.tooltip && this.$refs.tooltip.updatePopper();
|
||||
});
|
||||
if (!this.dragging && this.value !== this.oldValue) {
|
||||
this.oldValue = this.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
427
packages/slider/src/main.vue
Normal file
427
packages/slider/src/main.vue
Normal file
@@ -0,0 +1,427 @@
|
||||
<template>
|
||||
<div
|
||||
class="el-slider"
|
||||
:class="{ 'is-vertical': vertical, 'el-slider--with-input': showInput }"
|
||||
role="slider"
|
||||
:aria-valuemin="min"
|
||||
:aria-valuemax="max"
|
||||
:aria-orientation="vertical ? 'vertical': 'horizontal'"
|
||||
:aria-disabled="sliderDisabled"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="firstValue"
|
||||
v-if="showInput && !range"
|
||||
class="el-slider__input"
|
||||
ref="input"
|
||||
@change="emitChange"
|
||||
:step="step"
|
||||
:disabled="sliderDisabled"
|
||||
:controls="showInputControls"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:debounce="debounce"
|
||||
:size="inputSize">
|
||||
</el-input-number>
|
||||
<div
|
||||
class="el-slider__runway"
|
||||
:class="{ 'show-input': showInput, 'disabled': sliderDisabled }"
|
||||
:style="runwayStyle"
|
||||
@click="onSliderClick"
|
||||
ref="slider">
|
||||
<div
|
||||
class="el-slider__bar"
|
||||
:style="barStyle">
|
||||
</div>
|
||||
<slider-button
|
||||
:vertical="vertical"
|
||||
v-model="firstValue"
|
||||
:tooltip-class="tooltipClass"
|
||||
ref="button1">
|
||||
</slider-button>
|
||||
<slider-button
|
||||
:vertical="vertical"
|
||||
v-model="secondValue"
|
||||
:tooltip-class="tooltipClass"
|
||||
ref="button2"
|
||||
v-if="range">
|
||||
</slider-button>
|
||||
<div
|
||||
class="el-slider__stop"
|
||||
v-for="(item, key) in stops"
|
||||
:key="key"
|
||||
:style="getStopStyle(item)"
|
||||
v-if="showStops">
|
||||
</div>
|
||||
<template v-if="markList.length > 0">
|
||||
<div>
|
||||
<div
|
||||
v-for="(item, key) in markList"
|
||||
:style="getStopStyle(item.position)"
|
||||
class="el-slider__stop el-slider__marks-stop"
|
||||
:key="key">
|
||||
</div>
|
||||
</div>
|
||||
<div class="el-slider__marks">
|
||||
<slider-marker
|
||||
:mark="item.mark" v-for="(item, key) in markList"
|
||||
:key="key"
|
||||
:style="getStopStyle(item.position)">
|
||||
</slider-marker>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
import ElInputNumber from 'element-ui/packages/input-number';
|
||||
import SliderButton from './button.vue';
|
||||
import SliderMarker from './marker';
|
||||
import Emitter from 'element-ui/src/mixins/emitter';
|
||||
|
||||
export default {
|
||||
name: 'ElSlider',
|
||||
|
||||
mixins: [Emitter],
|
||||
|
||||
inject: {
|
||||
elForm: {
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
value: {
|
||||
type: [Number, Array],
|
||||
default: 0
|
||||
},
|
||||
showInput: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showInputControls: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
inputSize: {
|
||||
type: String,
|
||||
default: 'small'
|
||||
},
|
||||
showStops: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showTooltip: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
formatTooltip: Function,
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
range: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
height: {
|
||||
type: String
|
||||
},
|
||||
debounce: {
|
||||
type: Number,
|
||||
default: 300
|
||||
},
|
||||
label: {
|
||||
type: String
|
||||
},
|
||||
tooltipClass: String,
|
||||
marks: Object
|
||||
},
|
||||
|
||||
components: {
|
||||
ElInputNumber,
|
||||
SliderButton,
|
||||
SliderMarker
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
firstValue: null,
|
||||
secondValue: null,
|
||||
oldValue: null,
|
||||
dragging: false,
|
||||
sliderSize: 1
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(val, oldVal) {
|
||||
if (this.dragging ||
|
||||
Array.isArray(val) &&
|
||||
Array.isArray(oldVal) &&
|
||||
val.every((item, index) => item === oldVal[index])) {
|
||||
return;
|
||||
}
|
||||
this.setValues();
|
||||
},
|
||||
|
||||
dragging(val) {
|
||||
if (!val) {
|
||||
this.setValues();
|
||||
}
|
||||
},
|
||||
|
||||
firstValue(val) {
|
||||
if (this.range) {
|
||||
this.$emit('input', [this.minValue, this.maxValue]);
|
||||
} else {
|
||||
this.$emit('input', val);
|
||||
}
|
||||
},
|
||||
|
||||
secondValue() {
|
||||
if (this.range) {
|
||||
this.$emit('input', [this.minValue, this.maxValue]);
|
||||
}
|
||||
},
|
||||
|
||||
min() {
|
||||
this.setValues();
|
||||
},
|
||||
|
||||
max() {
|
||||
this.setValues();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
valueChanged() {
|
||||
if (this.range) {
|
||||
return ![this.minValue, this.maxValue]
|
||||
.every((item, index) => item === this.oldValue[index]);
|
||||
} else {
|
||||
return this.value !== this.oldValue;
|
||||
}
|
||||
},
|
||||
setValues() {
|
||||
if (this.min > this.max) {
|
||||
console.error('[Element Error][Slider]min should not be greater than max.');
|
||||
return;
|
||||
}
|
||||
const val = this.value;
|
||||
if (this.range && Array.isArray(val)) {
|
||||
if (val[1] < this.min) {
|
||||
this.$emit('input', [this.min, this.min]);
|
||||
} else if (val[0] > this.max) {
|
||||
this.$emit('input', [this.max, this.max]);
|
||||
} else if (val[0] < this.min) {
|
||||
this.$emit('input', [this.min, val[1]]);
|
||||
} else if (val[1] > this.max) {
|
||||
this.$emit('input', [val[0], this.max]);
|
||||
} else {
|
||||
this.firstValue = val[0];
|
||||
this.secondValue = val[1];
|
||||
if (this.valueChanged()) {
|
||||
this.dispatch('ElFormItem', 'el.form.change', [this.minValue, this.maxValue]);
|
||||
this.oldValue = val.slice();
|
||||
}
|
||||
}
|
||||
} else if (!this.range && typeof val === 'number' && !isNaN(val)) {
|
||||
if (val < this.min) {
|
||||
this.$emit('input', this.min);
|
||||
} else if (val > this.max) {
|
||||
this.$emit('input', this.max);
|
||||
} else {
|
||||
this.firstValue = val;
|
||||
if (this.valueChanged()) {
|
||||
this.dispatch('ElFormItem', 'el.form.change', val);
|
||||
this.oldValue = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setPosition(percent) {
|
||||
const targetValue = this.min + percent * (this.max - this.min) / 100;
|
||||
if (!this.range) {
|
||||
this.$refs.button1.setPosition(percent);
|
||||
return;
|
||||
}
|
||||
let button;
|
||||
if (Math.abs(this.minValue - targetValue) < Math.abs(this.maxValue - targetValue)) {
|
||||
button = this.firstValue < this.secondValue ? 'button1' : 'button2';
|
||||
} else {
|
||||
button = this.firstValue > this.secondValue ? 'button1' : 'button2';
|
||||
}
|
||||
this.$refs[button].setPosition(percent);
|
||||
},
|
||||
|
||||
onSliderClick(event) {
|
||||
if (this.sliderDisabled || this.dragging) return;
|
||||
this.resetSize();
|
||||
if (this.vertical) {
|
||||
const sliderOffsetBottom = this.$refs.slider.getBoundingClientRect().bottom;
|
||||
this.setPosition((sliderOffsetBottom - event.clientY) / this.sliderSize * 100);
|
||||
} else {
|
||||
const sliderOffsetLeft = this.$refs.slider.getBoundingClientRect().left;
|
||||
this.setPosition((event.clientX - sliderOffsetLeft) / this.sliderSize * 100);
|
||||
}
|
||||
this.emitChange();
|
||||
},
|
||||
|
||||
resetSize() {
|
||||
if (this.$refs.slider) {
|
||||
this.sliderSize = this.$refs.slider[`client${ this.vertical ? 'Height' : 'Width' }`];
|
||||
}
|
||||
},
|
||||
|
||||
emitChange() {
|
||||
this.$nextTick(() => {
|
||||
this.$emit('change', this.range ? [this.minValue, this.maxValue] : this.value);
|
||||
});
|
||||
},
|
||||
|
||||
getStopStyle(position) {
|
||||
return this.vertical ? { 'bottom': position + '%' } : { 'left': position + '%' };
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
stops() {
|
||||
if (!this.showStops || this.min > this.max) return [];
|
||||
if (this.step === 0) {
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
console.warn('[Element Warn][Slider]step should not be 0.');
|
||||
return [];
|
||||
}
|
||||
const stopCount = (this.max - this.min) / this.step;
|
||||
const stepWidth = 100 * this.step / (this.max - this.min);
|
||||
const result = [];
|
||||
for (let i = 1; i < stopCount; i++) {
|
||||
result.push(i * stepWidth);
|
||||
}
|
||||
if (this.range) {
|
||||
return result.filter(step => {
|
||||
return step < 100 * (this.minValue - this.min) / (this.max - this.min) ||
|
||||
step > 100 * (this.maxValue - this.min) / (this.max - this.min);
|
||||
});
|
||||
} else {
|
||||
return result.filter(step => step > 100 * (this.firstValue - this.min) / (this.max - this.min));
|
||||
}
|
||||
},
|
||||
|
||||
markList() {
|
||||
if (!this.marks) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const marksKeys = Object.keys(this.marks);
|
||||
return marksKeys.map(parseFloat)
|
||||
.sort((a, b) => a - b)
|
||||
.filter(point => point <= this.max && point >= this.min)
|
||||
.map(point => ({
|
||||
point,
|
||||
position: (point - this.min) * 100 / (this.max - this.min),
|
||||
mark: this.marks[point]
|
||||
}));
|
||||
},
|
||||
|
||||
minValue() {
|
||||
return Math.min(this.firstValue, this.secondValue);
|
||||
},
|
||||
|
||||
maxValue() {
|
||||
return Math.max(this.firstValue, this.secondValue);
|
||||
},
|
||||
|
||||
barSize() {
|
||||
return this.range
|
||||
? `${ 100 * (this.maxValue - this.minValue) / (this.max - this.min) }%`
|
||||
: `${ 100 * (this.firstValue - this.min) / (this.max - this.min) }%`;
|
||||
},
|
||||
|
||||
barStart() {
|
||||
return this.range
|
||||
? `${ 100 * (this.minValue - this.min) / (this.max - this.min) }%`
|
||||
: '0%';
|
||||
},
|
||||
|
||||
precision() {
|
||||
let precisions = [this.min, this.max, this.step].map(item => {
|
||||
let decimal = ('' + item).split('.')[1];
|
||||
return decimal ? decimal.length : 0;
|
||||
});
|
||||
return Math.max.apply(null, precisions);
|
||||
},
|
||||
|
||||
runwayStyle() {
|
||||
return this.vertical ? { height: this.height } : {};
|
||||
},
|
||||
|
||||
barStyle() {
|
||||
return this.vertical
|
||||
? {
|
||||
height: this.barSize,
|
||||
bottom: this.barStart
|
||||
} : {
|
||||
width: this.barSize,
|
||||
left: this.barStart
|
||||
};
|
||||
},
|
||||
|
||||
sliderDisabled() {
|
||||
return this.disabled || (this.elForm || {}).disabled;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
let valuetext;
|
||||
if (this.range) {
|
||||
if (Array.isArray(this.value)) {
|
||||
this.firstValue = Math.max(this.min, this.value[0]);
|
||||
this.secondValue = Math.min(this.max, this.value[1]);
|
||||
} else {
|
||||
this.firstValue = this.min;
|
||||
this.secondValue = this.max;
|
||||
}
|
||||
this.oldValue = [this.firstValue, this.secondValue];
|
||||
valuetext = `${this.firstValue}-${this.secondValue}`;
|
||||
} else {
|
||||
if (typeof this.value !== 'number' || isNaN(this.value)) {
|
||||
this.firstValue = this.min;
|
||||
} else {
|
||||
this.firstValue = Math.min(this.max, Math.max(this.min, this.value));
|
||||
}
|
||||
this.oldValue = this.firstValue;
|
||||
valuetext = this.firstValue;
|
||||
}
|
||||
this.$el.setAttribute('aria-valuetext', valuetext);
|
||||
|
||||
// label screen reader
|
||||
this.$el.setAttribute('aria-label', this.label ? this.label : `slider between ${this.min} and ${this.max}`);
|
||||
|
||||
this.resetSize();
|
||||
window.addEventListener('resize', this.resetSize);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.resetSize);
|
||||
}
|
||||
};
|
||||
</script>
|
18
packages/slider/src/marker.js
Normal file
18
packages/slider/src/marker.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export default {
|
||||
name: 'ElMarker',
|
||||
|
||||
props: {
|
||||
mark: {
|
||||
type: [String, Object]
|
||||
}
|
||||
},
|
||||
render() {
|
||||
let label = typeof this.mark === 'string' ? this.mark : this.mark.label;
|
||||
|
||||
return (
|
||||
<div class="el-slider__marks-text" style={ this.mark.style || {} }>
|
||||
{ label }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user