251 lines
9.1 KiB
Vue

<template>
<div
class="b-clockpicker-face"
@mousedown="onMouseDown"
@mouseup="onMouseUp"
@mousemove="onDragMove"
@touchstart="onMouseDown"
@touchend="onMouseUp"
@touchmove="onDragMove">
<div
class="b-clockpicker-face-outer-ring"
ref="clock">
<div
class="b-clockpicker-face-hand"
:style="handStyle" />
<span
v-for="(num, index) of faceNumbers"
:key="index"
class="b-clockpicker-face-number"
:class="getFaceNumberClasses(num)"
:style="{ transform: getNumberTranslate(num.value) }">
<span>{{ num.label }}</span>
</span>
</div>
</div>
</template>
<script>
// These should match the variables in clockpicker.scss
const indicatorSize = 40
const paddingInner = 5
export default {
name: 'BClockpickerFace',
props: {
pickerSize: Number,
min: Number,
max: Number,
double: Boolean,
value: Number,
faceNumbers: Array,
disabledValues: Function
},
data() {
return {
isDragging: false,
inputValue: this.value,
prevAngle: 720
}
},
computed: {
/**
* How many number indicators are shown on the face
*/
count() {
return this.max - this.min + 1
},
/**
* How many number indicators are shown per ring on the face
*/
countPerRing() {
return this.double ? (this.count / 2) : this.count
},
/**
* Radius of the clock face
*/
radius() {
return this.pickerSize / 2
},
/**
* Radius of the outer ring of number indicators
*/
outerRadius() {
return this.radius -
paddingInner -
indicatorSize / 2
},
/**
* Radius of the inner ring of number indicators
*/
innerRadius() {
return Math.max(this.outerRadius * 0.6,
this.outerRadius - paddingInner - indicatorSize)
// 48px gives enough room for the outer ring of numbers
},
/**
* The angle for each selectable value
* For hours this ends up being 30 degrees, for minutes 6 degrees
*/
degreesPerUnit() {
return 360 / this.countPerRing
},
/**
* Used for calculating x/y grid location based on degrees
*/
degrees() {
return this.degreesPerUnit * Math.PI / 180
},
/**
* Calculates the angle the clock hand should be rotated for the
* selected value
*/
handRotateAngle() {
let currentAngle = this.prevAngle
while (currentAngle < 0) currentAngle += 360
let targetAngle = this.calcHandAngle(this.displayedValue)
let degreesDiff = this.shortestDistanceDegrees(currentAngle, targetAngle)
let angle = this.prevAngle + degreesDiff
return angle
},
/**
* Determines how long the selector hand is based on if the
* selected value is located along the outer or inner ring
*/
handScale() {
return this.calcHandScale(this.displayedValue)
},
handStyle() {
return {
transform: `rotate(${this.handRotateAngle}deg) scaleY(${this.handScale})`,
transition: '.3s cubic-bezier(.25,.8,.50,1)'
}
},
/**
* The value the hand should be pointing at
*/
displayedValue() {
return this.inputValue == null ? this.min : this.inputValue
}
},
watch: {
value(value) {
if (value !== this.inputValue) {
this.prevAngle = this.handRotateAngle
}
this.inputValue = value
}
},
methods: {
isDisabled(value) {
return this.disabledValues && this.disabledValues(value)
},
/**
* Calculates the distance between two points
*/
euclidean(p0, p1) {
const dx = p1.x - p0.x
const dy = p1.y - p0.y
return Math.sqrt(dx * dx + dy * dy)
},
shortestDistanceDegrees(start, stop) {
const modDiff = (stop - start) % 360
let shortestDistance = 180 - Math.abs(Math.abs(modDiff) - 180)
return (modDiff + 360) % 360 < 180 ? shortestDistance * 1 : shortestDistance * -1
},
/**
* Calculates the angle of the line from the center point
* to the given point.
*/
coordToAngle(center, p1) {
const value = 2 *
Math.atan2(p1.y - center.y - this.euclidean(center, p1), p1.x - center.x)
return Math.abs(value * 180 / Math.PI)
},
/**
* Generates the inline style translate() property for a
* number indicator, which determines it's location on the
* clock face
*/
getNumberTranslate(value) {
const { x, y } = this.getNumberCoords(value)
return `translate(${x}px, ${y}px)`
},
/***
* Calculates the coordinates on the clock face for a number
* indicator value
*/
getNumberCoords(value) {
const radius = this.isInnerRing(value) ? this.innerRadius : this.outerRadius
return {
x: Math.round(radius * Math.sin((value - this.min) * this.degrees)),
y: Math.round(-radius * Math.cos((value - this.min) * this.degrees))
}
},
getFaceNumberClasses(num) {
return {
'active': num.value === this.displayedValue,
'disabled': this.isDisabled(num.value)
}
},
/**
* Determines if a value resides on the inner ring
*/
isInnerRing(value) {
return this.double && (value - this.min >= this.countPerRing)
},
calcHandAngle(value) {
let angle = this.degreesPerUnit * (value - this.min)
if (this.isInnerRing(value)) angle -= 360
return angle
},
calcHandScale(value) {
return this.isInnerRing(value)
? ((this.innerRadius) / this.outerRadius)
: 1
},
onMouseDown(e) {
e.preventDefault()
this.isDragging = true
this.onDragMove(e)
},
onMouseUp() {
this.isDragging = false
if (!this.isDisabled(this.inputValue)) {
this.$emit('change', this.inputValue)
}
},
onDragMove(e) {
e.preventDefault()
if (!this.isDragging && e.type !== 'click') return
const { width, top, left } = this.$refs.clock.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.coordToAngle(center, coords) + 360) % 360
const insideClick = this.double && this.euclidean(center, coords) <
(this.outerRadius + this.innerRadius) / 2 - 16
let value = Math.round(handAngle / this.degreesPerUnit) +
this.min +
(insideClick ? this.countPerRing : 0)
// Necessary to fix edge case when selecting left part of max value
if (handAngle >= (360 - this.degreesPerUnit / 2)) {
value = insideClick ? this.max : this.min
}
this.update(value)
},
update(value) {
if (this.inputValue !== value && !this.isDisabled(value)) {
this.prevAngle = this.handRotateAngle
this.inputValue = value
this.$emit('input', value)
}
}
}
}
</script>