quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
1,063 lines (919 loc) • 27.7 kB
JavaScript
import {
h,
ref,
computed,
watch,
withDirectives,
Transition,
nextTick,
getCurrentInstance
} from 'vue'
import QBtn from '../btn/QBtn.js'
import TouchPan from '../../directives/touch-pan/TouchPan.js'
import useDark, {
useDarkProps
} from '../../composables/private.use-dark/use-dark.js'
import {
useFormProps,
useFormAttrs,
useFormInject
} from '../../composables/use-form/private.use-form.js'
import useDatetime, {
useDatetimeProps,
useDatetimeEmits,
getDayHash
} from '../date/use-datetime.js'
import { createComponent } from '../../utils/private.create/create.js'
import { hSlot } from '../../utils/private.render/render.js'
import { formatDate, __splitDate } from '../../utils/date/date.js'
import { position } from '../../utils/event/event.js'
import { pad } from '../../utils/format/format.js'
import { vmIsDestroyed } from '../../utils/private.vm/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,
modelValue: {
required: true,
validator: val => typeof val === 'string' || val === null
},
mask: {
...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?.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(() => [
[
TouchPan,
onPan,
void 0,
{
stop: true,
prevent: true,
mouse: true
}
]
])
watch(
() => props.modelValue,
v => {
const val = __splitDate(
v,
mask.value,
locale.value,
props.calendar,
defaultDateModel.value
)
if (
val.dateHash !== innerModel.value.dateHash ||
val.timeHash !== innerModel.value.timeHash
) {
innerModel.value = val
if (val.hour === null) {
view.value = 'hour'
} else {
isAM.value = val.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.abs(pos.top - clockRect.top) ** 2 +
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) {
if (e.keyCode === 13) setAm()
}
function setPmOnKey(e) {
if (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 goToViewWhenHasModel(newView) {
const val = props.modelValue
if (
view.value !== newView &&
val !== void 0 &&
val !== null &&
val !== '' &&
typeof val !== 'string'
) {
view.value = newView
}
}
function verifyAndUpdate() {
if (
hourInSelection.value !== null &&
hourInSelection.value(innerModel.value.hour) !== true
) {
innerModel.value = __splitDate()
goToViewWhenHasModel('hour')
return
}
if (
minuteInSelection.value !== null &&
minuteInSelection.value(innerModel.value.minute) !== true
) {
innerModel.value.minute = null
innerModel.value.second = null
goToViewWhenHasModel('minute')
return
}
if (
props.withSeconds === true &&
secondInSelection.value !== null &&
secondInSelection.value(innerModel.value.second) !== true
) {
innerModel.value.second = null
goToViewWhenHasModel('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
)
]
if (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)
if (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)
]
)
}
}
})