quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
925 lines (783 loc) • 24.9 kB
JavaScript
import { h, ref, computed, watch, withDirectives, Transition, nextTick, getCurrentInstance } from 'vue'
import QBtn from '../btn/QBtn.js'
import TouchPan from '../../directives/TouchPan.js'
import useDark, { useDarkProps } from '../../composables/private/use-dark.js'
import { useFormProps, useFormAttrs, useFormInject } from '../../composables/private/use-form.js'
import useDatetime, { useDatetimeProps, useDatetimeEmits, getDayHash } from '../date/use-datetime.js'
import { createComponent } from '../../utils/private/create.js'
import { hSlot } from '../../utils/private/render.js'
import { formatDate, __splitDate } from '../../utils/date.js'
import { position } from '../../utils/event.js'
import { pad } from '../../utils/format.js'
import { vmIsDestroyed } from '../../utils/private/vm.js'
function getViewByModel (model, withSeconds) {
if (model.hour !== null) {
if (model.minute === null) {
return 'minute'
}
else if (withSeconds === true && model.second === null) {
return 'second'
}
}
return 'hour'
}
function getCurrentTime () {
const d = new Date()
return {
hour: d.getHours(),
minute: d.getMinutes(),
second: d.getSeconds(),
millisecond: d.getMilliseconds()
}
}
export default createComponent({
name: 'QTime',
props: {
...useDarkProps,
...useFormProps,
...useDatetimeProps,
mask: {
default: null
},
format24h: {
type: Boolean,
default: null
},
defaultDate: {
type: String,
validator: v => /^-?[\d]+\/[0-1]\d\/[0-3]\d$/.test(v)
},
options: Function,
hourOptions: Array,
minuteOptions: Array,
secondOptions: Array,
withSeconds: Boolean,
nowBtn: Boolean
},
emits: useDatetimeEmits,
setup (props, { slots, emit }) {
const vm = getCurrentInstance()
const { $q } = vm.proxy
const isDark = useDark(props, $q)
const { tabindex, headerClass, getLocale, getCurrentDate } = useDatetime(props, $q)
const formAttrs = useFormAttrs(props)
const injectFormInput = useFormInject(formAttrs)
let draggingClockRect, dragCache
const clockRef = ref(null)
const mask = computed(() => getMask())
const locale = computed(() => getLocale())
const defaultDateModel = computed(() => getDefaultDateModel())
const model = __splitDate(
props.modelValue,
mask.value, // initial mask
locale.value, // initial locale
props.calendar,
defaultDateModel.value
)
const view = ref(getViewByModel(model))
const innerModel = ref(model)
const isAM = ref(model.hour === null || model.hour < 12)
const classes = computed(() =>
`q-time q-time--${ props.landscape === true ? 'landscape' : 'portrait' }`
+ (isDark.value === true ? ' q-time--dark q-dark' : '')
+ (props.disable === true ? ' disabled' : (props.readonly === true ? ' q-time--readonly' : ''))
+ (props.bordered === true ? ' q-time--bordered' : '')
+ (props.square === true ? ' q-time--square no-border-radius' : '')
+ (props.flat === true ? ' q-time--flat no-shadow' : '')
)
const stringModel = computed(() => {
const time = innerModel.value
return {
hour: time.hour === null
? '--'
: (
computedFormat24h.value === true
? pad(time.hour)
: String(
isAM.value === true
? (time.hour === 0 ? 12 : time.hour)
: (time.hour > 12 ? time.hour - 12 : time.hour)
)
),
minute: time.minute === null
? '--'
: pad(time.minute),
second: time.second === null
? '--'
: pad(time.second)
}
})
const computedFormat24h = computed(() => (
props.format24h !== null
? props.format24h
: $q.lang.date.format24h
))
const pointerStyle = computed(() => {
const
forHour = view.value === 'hour',
divider = forHour === true ? 12 : 60,
amount = innerModel.value[ view.value ],
degrees = Math.round(amount * (360 / divider)) - 180
let transform = `rotate(${ degrees }deg) translateX(-50%)`
if (
forHour === true
&& computedFormat24h.value === true
&& innerModel.value.hour >= 12
) {
transform += ' scale(.7)'
}
return { transform }
})
const minLink = computed(() => innerModel.value.hour !== null)
const secLink = computed(() => minLink.value === true && innerModel.value.minute !== null)
const hourInSelection = computed(() => (
props.hourOptions !== void 0
? val => props.hourOptions.includes(val)
: (
props.options !== void 0
? val => props.options(val, null, null)
: null
)
))
const minuteInSelection = computed(() => (
props.minuteOptions !== void 0
? val => props.minuteOptions.includes(val)
: (
props.options !== void 0
? val => props.options(innerModel.value.hour, val, null)
: null
)
))
const secondInSelection = computed(() => (
props.secondOptions !== void 0
? val => props.secondOptions.includes(val)
: (
props.options !== void 0
? val => props.options(innerModel.value.hour, innerModel.value.minute, val)
: null
)
))
const validHours = computed(() => {
if (hourInSelection.value === null) {
return null
}
const am = getValidValues(0, 11, hourInSelection.value)
const pm = getValidValues(12, 11, hourInSelection.value)
return { am, pm, values: am.values.concat(pm.values) }
})
const validMinutes = computed(() => (
minuteInSelection.value !== null
? getValidValues(0, 59, minuteInSelection.value)
: null
))
const validSeconds = computed(() => (
secondInSelection.value !== null
? getValidValues(0, 59, secondInSelection.value)
: null
))
const viewValidOptions = computed(() => {
switch (view.value) {
case 'hour':
return validHours.value
case 'minute':
return validMinutes.value
case 'second':
return validSeconds.value
}
})
const positions = computed(() => {
let start, end, offset = 0, step = 1
const values = viewValidOptions.value !== null
? viewValidOptions.value.values
: void 0
if (view.value === 'hour') {
if (computedFormat24h.value === true) {
start = 0
end = 23
}
else {
start = 0
end = 11
if (isAM.value === false) {
offset = 12
}
}
}
else {
start = 0
end = 55
step = 5
}
const pos = []
for (let val = start, index = start; val <= end; val += step, index++) {
const
actualVal = val + offset,
disable = values !== void 0 && values.includes(actualVal) === false,
label = view.value === 'hour' && val === 0
? (computedFormat24h.value === true ? '00' : '12')
: val
pos.push({ val: actualVal, index, disable, label })
}
return pos
})
const clockDirectives = computed(() => {
return [ [
TouchPan,
onPan,
void 0,
{
stop: true,
prevent: true,
mouse: true
}
] ]
})
watch(() => props.modelValue, v => {
const model = __splitDate(
v,
mask.value,
locale.value,
props.calendar,
defaultDateModel.value
)
if (
model.dateHash !== innerModel.value.dateHash
|| model.timeHash !== innerModel.value.timeHash
) {
innerModel.value = model
if (model.hour === null) {
view.value = 'hour'
}
else {
isAM.value = model.hour < 12
}
}
})
watch([ mask, locale ], () => {
nextTick(() => {
updateValue()
})
})
function setNow () {
const date = {
...getCurrentDate(),
...getCurrentTime()
}
updateValue(date)
Object.assign(innerModel.value, date) // reset any pending changes to innerModel
view.value = 'hour'
}
function getValidValues (start, count, testFn) {
const values = Array.apply(null, { length: count + 1 })
.map((_, index) => {
const i = index + start
return {
index: i,
val: testFn(i) === true // force boolean
}
})
.filter(v => v.val === true)
.map(v => v.index)
return {
min: values[ 0 ],
max: values[ values.length - 1 ],
values,
threshold: count + 1
}
}
function getWheelDist (a, b, threshold) {
const diff = Math.abs(a - b)
return Math.min(diff, threshold - diff)
}
function getNormalizedClockValue (val, { min, max, values, threshold }) {
if (val === min) {
return min
}
if (val < min || val > max) {
return getWheelDist(val, min, threshold) <= getWheelDist(val, max, threshold)
? min
: max
}
const
index = values.findIndex(v => val <= v),
before = values[ index - 1 ],
after = values[ index ]
return val - before <= after - val
? before
: after
}
function getMask () {
return props.calendar !== 'persian' && props.mask !== null
? props.mask
: `HH:mm${ props.withSeconds === true ? ':ss' : '' }`
}
function getDefaultDateModel () {
if (typeof props.defaultDate !== 'string') {
const date = getCurrentDate(true)
date.dateHash = getDayHash(date)
return date
}
return __splitDate(props.defaultDate, 'YYYY/MM/DD', void 0, props.calendar)
}
function shouldAbortInteraction () {
return vmIsDestroyed(vm) === true
// if we have limited options, can we actually set any?
|| (
viewValidOptions.value !== null
&& (
viewValidOptions.value.values.length === 0
|| (
view.value === 'hour' && computedFormat24h.value !== true
&& validHours.value[ isAM.value === true ? 'am' : 'pm' ].values.length === 0
)
)
)
}
function getClockRect () {
const
clock = clockRef.value,
{ top, left, width } = clock.getBoundingClientRect(),
dist = width / 2
return {
top: top + dist,
left: left + dist,
dist: dist * 0.7
}
}
function onPan (event) {
if (shouldAbortInteraction() === true) {
return
}
if (event.isFirst === true) {
draggingClockRect = getClockRect()
dragCache = updateClock(event.evt, draggingClockRect)
return
}
dragCache = updateClock(event.evt, draggingClockRect, dragCache)
if (event.isFinal === true) {
draggingClockRect = false
dragCache = null
goToNextView()
}
}
function goToNextView () {
if (view.value === 'hour') {
view.value = 'minute'
}
else if (props.withSeconds && view.value === 'minute') {
view.value = 'second'
}
}
function updateClock (evt, clockRect, cacheVal) {
const
pos = position(evt),
height = Math.abs(pos.top - clockRect.top),
distance = Math.sqrt(
Math.pow(Math.abs(pos.top - clockRect.top), 2)
+ Math.pow(Math.abs(pos.left - clockRect.left), 2)
)
let
val,
angle = Math.asin(height / distance) * (180 / Math.PI)
if (pos.top < clockRect.top) {
angle = clockRect.left < pos.left ? 90 - angle : 270 + angle
}
else {
angle = clockRect.left < pos.left ? angle + 90 : 270 - angle
}
if (view.value === 'hour') {
val = angle / 30
if (validHours.value !== null) {
const am = computedFormat24h.value !== true
? isAM.value === true
: (
validHours.value.am.values.length > 0 && validHours.value.pm.values.length > 0
? distance >= clockRect.dist
: validHours.value.am.values.length > 0
)
val = getNormalizedClockValue(
val + (am === true ? 0 : 12),
validHours.value[ am === true ? 'am' : 'pm' ]
)
}
else {
val = Math.round(val)
if (computedFormat24h.value === true) {
if (distance < clockRect.dist) {
if (val < 12) {
val += 12
}
}
else if (val === 12) {
val = 0
}
}
else if (isAM.value === true && val === 12) {
val = 0
}
else if (isAM.value === false && val !== 12) {
val += 12
}
}
if (computedFormat24h.value === true) {
isAM.value = val < 12
}
}
else {
val = Math.round(angle / 6) % 60
if (view.value === 'minute' && validMinutes.value !== null) {
val = getNormalizedClockValue(val, validMinutes.value)
}
else if (view.value === 'second' && validSeconds.value !== null) {
val = getNormalizedClockValue(val, validSeconds.value)
}
}
if (cacheVal !== val) {
setModel[ view.value ](val)
}
return val
}
const setView = {
hour () { view.value = 'hour' },
minute () { view.value = 'minute' },
second () { view.value = 'second' }
}
function setAmOnKey (e) {
e.keyCode === 13 && setAm()
}
function setPmOnKey (e) {
e.keyCode === 13 && setPm()
}
function onClick (evt) {
if (shouldAbortInteraction() !== true) {
// onMousedown() has already updated the offset
// (on desktop only, through mousedown event)
if ($q.platform.is.desktop !== true) {
updateClock(evt, getClockRect())
}
goToNextView()
}
}
function onMousedown (evt) {
if (shouldAbortInteraction() !== true) {
updateClock(evt, getClockRect())
}
}
function onKeyupHour (e) {
if (e.keyCode === 13) { // ENTER
view.value = 'hour'
}
else if ([ 37, 39 ].includes(e.keyCode)) {
const payload = e.keyCode === 37 ? -1 : 1
if (validHours.value !== null) {
const values = computedFormat24h.value === true
? validHours.value.values
: validHours.value[ isAM.value === true ? 'am' : 'pm' ].values
if (values.length === 0) { return }
if (innerModel.value.hour === null) {
setHour(values[ 0 ])
}
else {
const index = (
values.length
+ values.indexOf(innerModel.value.hour)
+ payload
) % values.length
setHour(values[ index ])
}
}
else {
const
wrap = computedFormat24h.value === true ? 24 : 12,
offset = computedFormat24h.value !== true && isAM.value === false ? 12 : 0,
val = innerModel.value.hour === null ? -payload : innerModel.value.hour
setHour(offset + (24 + val + payload) % wrap)
}
}
}
function onKeyupMinute (e) {
if (e.keyCode === 13) { // ENTER
view.value = 'minute'
}
else if ([ 37, 39 ].includes(e.keyCode)) {
const payload = e.keyCode === 37 ? -1 : 1
if (validMinutes.value !== null) {
const values = validMinutes.value.values
if (values.length === 0) { return }
if (innerModel.value.minute === null) {
setMinute(values[ 0 ])
}
else {
const index = (
values.length
+ values.indexOf(innerModel.value.minute)
+ payload
) % values.length
setMinute(values[ index ])
}
}
else {
const val = innerModel.value.minute === null ? -payload : innerModel.value.minute
setMinute((60 + val + payload) % 60)
}
}
}
function onKeyupSecond (e) {
if (e.keyCode === 13) { // ENTER
view.value = 'second'
}
else if ([ 37, 39 ].includes(e.keyCode)) {
const payload = e.keyCode === 37 ? -1 : 1
if (validSeconds.value !== null) {
const values = validSeconds.value.values
if (values.length === 0) { return }
if (innerModel.value.seconds === null) {
setSecond(values[ 0 ])
}
else {
const index = (
values.length
+ values.indexOf(innerModel.value.second)
+ payload
) % values.length
setSecond(values[ index ])
}
}
else {
const val = innerModel.value.second === null ? -payload : innerModel.value.second
setSecond((60 + val + payload) % 60)
}
}
}
function setHour (hour) {
if (innerModel.value.hour !== hour) {
innerModel.value.hour = hour
verifyAndUpdate()
}
}
function setMinute (minute) {
if (innerModel.value.minute !== minute) {
innerModel.value.minute = minute
verifyAndUpdate()
}
}
function setSecond (second) {
if (innerModel.value.second !== second) {
innerModel.value.second = second
verifyAndUpdate()
}
}
const setModel = {
hour: setHour,
minute: setMinute,
second: setSecond
}
function setAm () {
if (isAM.value === false) {
isAM.value = true
if (innerModel.value.hour !== null) {
innerModel.value.hour -= 12
verifyAndUpdate()
}
}
}
function setPm () {
if (isAM.value === true) {
isAM.value = false
if (innerModel.value.hour !== null) {
innerModel.value.hour += 12
verifyAndUpdate()
}
}
}
function verifyAndUpdate () {
if (hourInSelection.value !== null && hourInSelection.value(innerModel.value.hour) !== true) {
innerModel.value = __splitDate()
view.value = 'hour'
return
}
if (minuteInSelection.value !== null && minuteInSelection.value(innerModel.value.minute) !== true) {
innerModel.value.minute = null
innerModel.value.second = null
view.value = 'minute'
return
}
if (props.withSeconds === true && secondInSelection.value !== null && secondInSelection.value(innerModel.value.second) !== true) {
innerModel.value.second = null
view.value = 'second'
return
}
if (innerModel.value.hour === null || innerModel.value.minute === null || (props.withSeconds === true && innerModel.value.second === null)) {
return
}
updateValue()
}
function updateValue (obj) {
const date = Object.assign({ ...innerModel.value }, obj)
const val = props.calendar === 'persian'
? pad(date.hour) + ':'
+ pad(date.minute)
+ (props.withSeconds === true ? ':' + pad(date.second) : '')
: formatDate(
new Date(
date.year,
date.month === null ? null : date.month - 1,
date.day,
date.hour,
date.minute,
date.second,
date.millisecond
),
mask.value,
locale.value,
date.year,
date.timezoneOffset
)
date.changed = val !== props.modelValue
emit('update:modelValue', val, date)
}
function getHeader () {
const label = [
h('div', {
class: 'q-time__link '
+ (view.value === 'hour' ? 'q-time__link--active' : 'cursor-pointer'),
tabindex: tabindex.value,
onClick: setView.hour,
onKeyup: onKeyupHour
}, stringModel.value.hour),
h('div', ':'),
h(
'div',
minLink.value === true
? {
class: 'q-time__link '
+ (view.value === 'minute' ? 'q-time__link--active' : 'cursor-pointer'),
tabindex: tabindex.value,
onKeyup: onKeyupMinute,
onClick: setView.minute
}
: { class: 'q-time__link' },
stringModel.value.minute
)
]
if (props.withSeconds === true) {
label.push(
h('div', ':'),
h(
'div',
secLink.value === true
? {
class: 'q-time__link '
+ (view.value === 'second' ? 'q-time__link--active' : 'cursor-pointer'),
tabindex: tabindex.value,
onKeyup: onKeyupSecond,
onClick: setView.second
}
: { class: 'q-time__link' },
stringModel.value.second
)
)
}
const child = [
h('div', {
class: 'q-time__header-label row items-center no-wrap',
dir: 'ltr'
}, label)
]
computedFormat24h.value === false && child.push(
h('div', {
class: 'q-time__header-ampm column items-between no-wrap'
}, [
h('div', {
class: 'q-time__link '
+ (isAM.value === true ? 'q-time__link--active' : 'cursor-pointer'),
tabindex: tabindex.value,
onClick: setAm,
onKeyup: setAmOnKey
}, 'AM'),
h('div', {
class: 'q-time__link '
+ (isAM.value !== true ? 'q-time__link--active' : 'cursor-pointer'),
tabindex: tabindex.value,
onClick: setPm,
onKeyup: setPmOnKey
}, 'PM')
])
)
return h('div', {
class: 'q-time__header flex flex-center no-wrap ' + headerClass.value
}, child)
}
function getClock () {
const current = innerModel.value[ view.value ]
return h('div', {
class: 'q-time__content col relative-position'
}, [
h(Transition, {
name: 'q-transition--scale'
}, () => h('div', {
key: 'clock' + view.value,
class: 'q-time__container-parent absolute-full'
}, [
h('div', {
ref: clockRef,
class: 'q-time__container-child fit overflow-hidden'
}, [
withDirectives(
h('div', {
class: 'q-time__clock cursor-pointer non-selectable',
onClick,
onMousedown
}, [
h('div', { class: 'q-time__clock-circle fit' }, [
h('div', {
class: 'q-time__clock-pointer'
+ (innerModel.value[ view.value ] === null ? ' hidden' : (props.color !== void 0 ? ` text-${ props.color }` : '')),
style: pointerStyle.value
}),
positions.value.map(pos => h('div', {
class: `q-time__clock-position row flex-center q-time__clock-pos-${ pos.index }`
+ (pos.val === current
? ' q-time__clock-position--active ' + headerClass.value
: (pos.disable === true ? ' q-time__clock-position--disable' : ''))
}, [ h('span', pos.label) ]))
])
]),
clockDirectives.value
)
])
])),
props.nowBtn === true ? h(QBtn, {
class: 'q-time__now-button absolute',
icon: $q.iconSet.datetime.now,
unelevated: true,
size: 'sm',
round: true,
color: props.color,
textColor: props.textColor,
tabindex: tabindex.value,
onClick: setNow
}) : null
])
}
// expose public method
vm.proxy.setNow = setNow
return () => {
const child = [ getClock() ]
const def = hSlot(slots.default)
def !== void 0 && child.push(
h('div', { class: 'q-time__actions' }, def)
)
if (props.name !== void 0 && props.disable !== true) {
injectFormInput(child, 'push')
}
return h('div', {
class: classes.value,
tabindex: -1
}, [
getHeader(),
h('div', { class: 'q-time__main col overflow-auto' }, child)
])
}
}
})