UNPKG

vuetify

Version:

Vue.js 2 Semantic Component Framework

350 lines (315 loc) 9.53 kB
require('../../stylus/components/_sliders.styl') import { addOnceEventListener, createRange } from '../../util/helpers' import Colorable from '../../mixins/colorable' import Input from '../../mixins/input' import ClickOutside from '../../directives/click-outside' import { VScaleTransition } from '../transitions' import { consoleWarn } from '../../util/console' export default { name: 'v-slider', mixins: [Colorable, Input], directives: { ClickOutside }, components: { VScaleTransition }, data () { return { app: {}, defaultColor: 'primary', isActive: false, keyPressed: 0 } }, props: { min: { type: [Number, String], default: 0 }, max: { type: [Number, String], default: 100 }, step: { type: [Number, String], default: 1 }, ticks: Boolean, thumbColor: { type: String, default: null }, thumbLabel: Boolean, trackColor: { type: String, default: null }, value: [Number, String] }, computed: { classes () { return { 'input-group--slider': true, 'input-group--active': this.isActive, 'input-group--dirty': this.inputWidth > 0, 'input-group--disabled': this.disabled, 'input-group--ticks': !this.disabled && this.stepNumeric && this.ticks } }, computedColor () { return this.disabled ? null : (this.color || this.defaultColor) }, computedTrackColor () { return this.disabled ? null : (this.trackColor || null) }, computedThumbColor () { return (this.disabled || !this.inputWidth) ? null : (this.thumbColor || this.color || this.defaultColor) }, stepNumeric () { return this.step > 0 ? parseFloat(this.step) : 0 }, inputValue: { get () { return this.value }, set (val) { const { min, max } = this val = Math.min(Math.max(val, min), max) // Round value to ensure the // entire slider range can // be selected with step const value = this.roundValue(val) this.lazyValue = value if (value !== this.value) { this.$emit('input', value) } } }, interval () { return 100 / (this.max - this.min) * this.stepNumeric }, thumbStyles () { return { transition: this.keyPressed >= 2 ? 'none' : '', left: `${this.inputWidth}%` } }, tickContainerStyles () { return { transform: `translate(0, -50%)` } }, trackPadding () { if (this.thumbLabel && this.isActive) return 0 return 6 + (this.isActive && !this.disabled ? 3 : 0) }, trackStyles () { return { transition: this.keyPressed >= 2 ? 'none' : '', left: `calc(${this.inputWidth}% + ${this.trackPadding}px)`, width: `calc(${100 - this.inputWidth}% - ${this.trackPadding}px)` } }, trackFillStyles () { return { transition: this.keyPressed >= 2 ? 'none' : '', width: `calc(${this.inputWidth}% - ${this.trackPadding}px)` } }, numTicks () { return Math.ceil((this.max - this.min) / this.stepNumeric) }, inputWidth () { return (this.roundValue(this.inputValue) - this.min) / (this.max - this.min) * 100 } }, watch: { isActive (val) { this.isFocused = val }, min (val) { val > this.inputValue && this.$emit('input', parseFloat(val)) }, max (val) { val < this.inputValue && this.$emit('input', parseFloat(val)) }, value (val) { this.inputValue = parseFloat(val) } }, mounted () { this.inputValue = this.value // Without a v-app, iOS does not work with body selectors this.app = document.querySelector('[data-app]') || consoleWarn('Missing v-app or a non-body wrapping element with the [data-app] attribute', this) }, methods: { onMouseDown (e) { this.keyPressed = 2 const options = { passive: true } this.isActive = true if ('touches' in e) { this.app.addEventListener('touchmove', this.onMouseMove, options) addOnceEventListener(this.app, 'touchend', this.onMouseUp) } else { this.app.addEventListener('mousemove', this.onMouseMove, options) addOnceEventListener(this.app, 'mouseup', this.onMouseUp) } }, onMouseUp () { this.keyPressed = 0 const options = { passive: true } this.isActive = false this.app.removeEventListener('touchmove', this.onMouseMove, options) this.app.removeEventListener('mousemove', this.onMouseMove, options) }, onMouseMove (e) { const { left: offsetLeft, width: trackWidth } = this.$refs.track.getBoundingClientRect() const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX const left = Math.min(Math.max((clientX - offsetLeft) / trackWidth, 0), 1) if (clientX >= offsetLeft - 8 && clientX <= offsetLeft + trackWidth + 8) { this.inputValue = parseFloat(this.min) + left * (this.max - this.min) } }, onKeyDown (e) { if (this.disabled || ![33, 34, 35, 36, 37, 39].includes(e.keyCode)) return e.preventDefault() const step = this.stepNumeric || 1 const steps = (this.max - this.min) / step if (e.keyCode === 37 || e.keyCode === 39) { // Left/right this.keyPressed += 1 const direction = e.keyCode === 37 ? -1 : 1 const multiplier = e.shiftKey ? 3 : (e.ctrlKey ? 2 : 1) this.inputValue = this.inputValue + direction * step * multiplier } else if (e.keyCode === 36) { // Home this.inputValue = parseFloat(this.min) } else if (e.keyCode === 35) { // End this.inputValue = parseFloat(this.max) } else /* if (e.keyCode === 33 || e.keyCode === 34) */ { // Page up/down const direction = e.keyCode === 34 ? -1 : 1 this.inputValue = this.inputValue - direction * step * (steps > 100 ? steps / 10 : 10) } }, onKeyUp (e) { this.keyPressed = 0 }, sliderMove (e) { if (!this.isActive) { this.onMouseMove(e) } }, genThumbLabel (h) { return h('v-scale-transition', { props: { origin: 'bottom center' } }, [ h('div', { staticClass: 'slider__thumb--label__container', directives: [ { name: 'show', value: this.isActive } ] }, [ h('div', { staticClass: 'slider__thumb--label', 'class': this.addBackgroundColorClassChecks({}, this.computedThumbColor) }, [ h('span', {}, this.inputValue) ]) ]) ]) }, roundValue (value) { if (!this.stepNumeric) { return value } // Format input value using the same number // of decimals places as in the step prop const trimmedStep = this.step.toString().trim() const decimals = trimmedStep.indexOf('.') > -1 ? (trimmedStep.length - trimmedStep.indexOf('.') - 1) : 0 return 1 * (Math.round(value / this.stepNumeric) * this.stepNumeric).toFixed(decimals) }, genThumbContainer (h) { const children = [] children.push(h('div', { staticClass: 'slider__thumb', 'class': this.addBackgroundColorClassChecks({}, this.computedThumbColor) })) this.thumbLabel && children.push(this.genThumbLabel(h)) return h('div', { staticClass: 'slider__thumb-container', 'class': { 'slider__thumb-container--label': this.thumbLabel }, style: this.thumbStyles, on: { touchstart: this.onMouseDown, mousedown: this.onMouseDown }, ref: 'thumb' }, children) }, genSteps (h) { const ticks = createRange(this.numTicks + 1).map((i) => { const span = h('span', { key: i, staticClass: 'slider__tick', style: { left: `${i * (100 / this.numTicks)}%` } }) return span }) return h('div', { staticClass: 'slider__ticks-container', style: this.tickContainerStyles }, ticks) }, genTrackContainer (h) { const children = [ h('div', { staticClass: 'slider__track', 'class': this.addBackgroundColorClassChecks({}, this.computedTrackColor), style: this.trackStyles }), h('div', { staticClass: 'slider__track-fill', 'class': this.addBackgroundColorClassChecks(), style: this.trackFillStyles }) ] return h('div', { staticClass: 'slider__track__container', ref: 'track' }, children) } }, render (h) { const children = [] children.push(this.genTrackContainer(h)) this.step && this.ticks && children.push(this.genSteps(h)) children.push(this.genThumbContainer(h)) const slider = h('div', { staticClass: 'slider' }, children) return this.genInputGroup([slider], { attrs: { role: 'slider', tabindex: this.disabled ? -1 : this.tabindex }, on: Object.assign({}, { mouseup: this.sliderMove, keydown: this.onKeyDown, keyup: this.onKeyUp }, this.$listeners), directives: [{ name: 'click-outside' }] }) } }