251 lines
9.1 KiB
Vue
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>
|