UNPKG

vuetify

Version:

Vue Material Component Framework

283 lines (256 loc) 8.14 kB
import './VTimePickerClock.sass' // Mixins import Colorable from '../../mixins/colorable' import Themeable from '../../mixins/themeable' // Types import mixins, { ExtractVue } from '../../util/mixins' import Vue, { VNode, PropType, VNodeData } from 'vue' import { PropValidator } from 'vue/types/options' interface Point { x: number y: number } interface options extends Vue { $refs: { clock: HTMLElement innerClock: HTMLElement } } export default mixins<options & /* eslint-disable indent */ ExtractVue<[ typeof Colorable, typeof Themeable ]> /* eslint-enable indent */ >( Colorable, Themeable /* @vue/component */ ).extend({ name: 'v-time-picker-clock', props: { allowedValues: Function as PropType<(value: number) => boolean>, ampm: Boolean, disabled: Boolean, double: Boolean, format: { type: Function, default: (val: string | number) => val, } as PropValidator<(val: string | number) => string | number>, max: { type: Number, required: true, }, min: { type: Number, required: true, }, scrollable: Boolean, readonly: Boolean, rotate: { type: Number, default: 0, }, step: { type: Number, default: 1, }, value: Number, }, data () { return { inputValue: this.value, isDragging: false, valueOnMouseDown: null as number | null, valueOnMouseUp: null as number | null, } }, computed: { count (): number { return this.max - this.min + 1 }, degreesPerUnit (): number { return 360 / this.roundCount }, degrees (): number { return this.degreesPerUnit * Math.PI / 180 }, displayedValue (): number { return this.value == null ? this.min : this.value }, innerRadiusScale (): number { return 0.62 }, roundCount (): number { return this.double ? (this.count / 2) : this.count }, }, watch: { value (value) { this.inputValue = value }, }, methods: { wheel (e: WheelEvent) { e.preventDefault() const delta = Math.sign(-e.deltaY || 1) let value = this.displayedValue do { value = value + delta value = (value - this.min + this.count) % this.count + this.min } while (!this.isAllowed(value) && value !== this.displayedValue) if (value !== this.displayedValue) { this.update(value) } }, isInner (value: number) { return this.double && (value - this.min >= this.roundCount) }, handScale (value: number) { return this.isInner(value) ? this.innerRadiusScale : 1 }, isAllowed (value: number) { return !this.allowedValues || this.allowedValues(value) }, genValues () { const children: VNode[] = [] for (let value = this.min; value <= this.max; value = value + this.step) { const color = value === this.value && (this.color || 'accent') children.push(this.$createElement('span', this.setBackgroundColor(color, { staticClass: 'v-time-picker-clock__item', class: { 'v-time-picker-clock__item--active': value === this.displayedValue, 'v-time-picker-clock__item--disabled': this.disabled || !this.isAllowed(value), }, style: this.getTransform(value), domProps: { innerHTML: `<span>${this.format(value)}</span>` }, }))) } return children }, genHand () { const scale = `scaleY(${this.handScale(this.displayedValue)})` const angle = this.rotate + this.degreesPerUnit * (this.displayedValue - this.min) const color = (this.value != null) && (this.color || 'accent') return this.$createElement('div', this.setBackgroundColor(color, { staticClass: 'v-time-picker-clock__hand', class: { 'v-time-picker-clock__hand--inner': this.isInner(this.value), }, style: { transform: `rotate(${angle}deg) ${scale}`, }, })) }, getTransform (i: number) { const { x, y } = this.getPosition(i) return { left: `${50 + x * 50}%`, top: `${50 + y * 50}%`, } }, getPosition (value: number) { const rotateRadians = this.rotate * Math.PI / 180 return { x: Math.sin((value - this.min) * this.degrees + rotateRadians) * this.handScale(value), y: -Math.cos((value - this.min) * this.degrees + rotateRadians) * this.handScale(value), } }, onMouseDown (e: MouseEvent | TouchEvent) { e.preventDefault() this.valueOnMouseDown = null this.valueOnMouseUp = null this.isDragging = true this.onDragMove(e) }, onMouseUp (e: MouseEvent | TouchEvent) { e.stopPropagation() this.isDragging = false if (this.valueOnMouseUp !== null && this.isAllowed(this.valueOnMouseUp)) { this.$emit('change', this.valueOnMouseUp) } }, onDragMove (e: MouseEvent | TouchEvent) { e.preventDefault() if (!this.isDragging && e.type !== 'click') return const { width, top, left } = this.$refs.clock.getBoundingClientRect() const { width: innerWidth } = this.$refs.innerClock.getBoundingClientRect() const { clientX, clientY } = 'touches' in e ? e.touches[0] : e const center = { x: width / 2, y: -width / 2 } const coords = { x: clientX - left, y: top - clientY } const handAngle = Math.round(this.angle(center, coords) - this.rotate + 360) % 360 const insideClick = this.double && this.euclidean(center, coords) < (innerWidth + innerWidth * this.innerRadiusScale) / 4 const checksCount = Math.ceil(15 / this.degreesPerUnit) let value for (let i = 0; i < checksCount; i++) { value = this.angleToValue(handAngle + i * this.degreesPerUnit, insideClick) if (this.isAllowed(value)) return this.setMouseDownValue(value) value = this.angleToValue(handAngle - i * this.degreesPerUnit, insideClick) if (this.isAllowed(value)) return this.setMouseDownValue(value) } }, angleToValue (angle: number, insideClick: boolean): number { const value = ( Math.round(angle / this.degreesPerUnit) + (insideClick ? this.roundCount : 0) ) % this.count + this.min // Necessary to fix edge case when selecting left part of the value(s) at 12 o'clock if (angle < (360 - this.degreesPerUnit / 2)) return value return insideClick ? this.max - this.roundCount + 1 : this.min }, setMouseDownValue (value: number) { if (this.valueOnMouseDown === null) { this.valueOnMouseDown = value } this.valueOnMouseUp = value this.update(value) }, update (value: number) { if (this.inputValue !== value) { this.inputValue = value this.$emit('input', value) } }, euclidean (p0: Point, p1: Point) { const dx = p1.x - p0.x const dy = p1.y - p0.y return Math.sqrt(dx * dx + dy * dy) }, angle (center: Point, p1: Point) { const value = 2 * Math.atan2(p1.y - center.y - this.euclidean(center, p1), p1.x - center.x) return Math.abs(value * 180 / Math.PI) }, }, render (h): VNode { const data: VNodeData = { staticClass: 'v-time-picker-clock', class: { 'v-time-picker-clock--indeterminate': this.value == null, ...this.themeClasses, }, on: (this.readonly || this.disabled) ? undefined : { mousedown: this.onMouseDown, mouseup: this.onMouseUp, mouseleave: (e: MouseEvent) => (this.isDragging && this.onMouseUp(e)), touchstart: this.onMouseDown, touchend: this.onMouseUp, mousemove: this.onDragMove, touchmove: this.onDragMove, }, ref: 'clock', } if (this.scrollable && data.on) { data.on.wheel = this.wheel } return h('div', data, [ h('div', { staticClass: 'v-time-picker-clock__inner', ref: 'innerClock', }, [ this.genHand(), this.genValues(), ]), ]) }, })