add time formats
This commit is contained in:
@@ -161,9 +161,7 @@ module Api
|
||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||
end
|
||||
|
||||
submitter.values = submitter.values.transform_values do |v|
|
||||
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
|
||||
end
|
||||
submitter.values = Submitters::SubmitValues.replace_current_date_placeholders(submitter)
|
||||
end
|
||||
|
||||
submitter
|
||||
|
||||
@@ -527,7 +527,8 @@ export default {
|
||||
try {
|
||||
return this.formatDate(
|
||||
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
||||
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'),
|
||||
{ withTimePlaceholders: this.modelValue === '{{date}}' }
|
||||
)
|
||||
} catch {
|
||||
return this.modelValue
|
||||
@@ -646,36 +647,55 @@ export default {
|
||||
return number
|
||||
}
|
||||
},
|
||||
formatDate (date, format) {
|
||||
const monthFormats = {
|
||||
M: 'numeric',
|
||||
MM: '2-digit',
|
||||
MMM: 'short',
|
||||
MMMM: 'long'
|
||||
}
|
||||
formatDate (date, format, { withTimePlaceholders = false } = {}) {
|
||||
const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
|
||||
const dayFormats = { D: 'numeric', DD: '2-digit' }
|
||||
const yearFormats = { YYYY: 'numeric', YYY: 'numeric', YY: '2-digit' }
|
||||
const hourFormats = { H: 'numeric', HH: '2-digit', h: 'numeric', hh: '2-digit' }
|
||||
const minuteFormats = { m: 'numeric', mm: '2-digit' }
|
||||
const secondFormats = { s: 'numeric', ss: '2-digit' }
|
||||
|
||||
const dayFormats = {
|
||||
D: 'numeric',
|
||||
DD: '2-digit'
|
||||
}
|
||||
const hasTime = /[HhAasz]/.test(format)
|
||||
|
||||
const yearFormats = {
|
||||
YYYY: 'numeric',
|
||||
YYY: 'numeric',
|
||||
YY: '2-digit'
|
||||
}
|
||||
|
||||
const parts = new Intl.DateTimeFormat([], {
|
||||
const opts = {
|
||||
day: dayFormats[format.match(/D+/)],
|
||||
month: monthFormats[format.match(/M+/)],
|
||||
year: yearFormats[format.match(/Y+/)],
|
||||
timeZone: 'UTC'
|
||||
}).formatToParts(date)
|
||||
year: yearFormats[format.match(/Y+/)]
|
||||
}
|
||||
|
||||
return format
|
||||
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
||||
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
||||
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
||||
if (format.match(/H+/)) { opts.hour = hourFormats[format.match(/H+/)[0]]; opts.hour12 = false }
|
||||
if (format.match(/h+/)) { opts.hour = hourFormats[format.match(/h+/)[0]]; opts.hour12 = true }
|
||||
if (/[Aa]/.test(format) && opts.hour12 === undefined) opts.hour12 = true
|
||||
if (format.match(/m+/)) opts.minute = minuteFormats[format.match(/m+/)[0]]
|
||||
if (format.match(/s+/)) opts.second = secondFormats[format.match(/s+/)[0]]
|
||||
if (/z/.test(format)) opts.timeZoneName = 'short'
|
||||
if (!hasTime) opts.timeZone = 'UTC'
|
||||
|
||||
const partTypes = {
|
||||
M: 'month',
|
||||
D: 'day',
|
||||
Y: 'year',
|
||||
H: 'hour',
|
||||
h: 'hour',
|
||||
m: 'minute',
|
||||
s: 'second',
|
||||
z: 'timeZoneName',
|
||||
A: 'dayPeriod',
|
||||
a: 'dayPeriod'
|
||||
}
|
||||
|
||||
const parts = new Intl.DateTimeFormat([], opts).formatToParts(date)
|
||||
|
||||
return format.replace(/MMMM|MMM|MM|M|DD|D|YYYY|YYY|YY|HH|hh|H|h|mm|m|ss|s|A|a|z/g, (token) => {
|
||||
if (withTimePlaceholders && /^(HH|hh|H|h|mm|m|ss|s|A|a)$/.test(token)) return '--'
|
||||
|
||||
const value = parts.find((p) => p.type === partTypes[token[0]])?.value
|
||||
|
||||
if (token === 'A') return (value || '').toUpperCase()
|
||||
if (token === 'a') return (value || '').toLowerCase()
|
||||
|
||||
return value
|
||||
})
|
||||
},
|
||||
updateMultipleSelectValue (value) {
|
||||
if (this.modelValue?.includes(value)) {
|
||||
|
||||
@@ -56,12 +56,18 @@
|
||||
class="base-input !text-2xl text-center w-full"
|
||||
:required="field.required"
|
||||
:aria-describedby="field.description ? field.uuid + '-desc' : undefined"
|
||||
type="date"
|
||||
:name="`values[${field.uuid}]`"
|
||||
:type="inputType"
|
||||
:name="formatType === 'datetime' ? undefined : `values[${field.uuid}]`"
|
||||
@keydown.enter="onEnter"
|
||||
@focus="$emit('focus')"
|
||||
@paste="onPaste"
|
||||
>
|
||||
<input
|
||||
v-if="formatType === 'datetime'"
|
||||
type="hidden"
|
||||
:name="`values[${field.uuid}]`"
|
||||
:value="modelValue"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -97,14 +103,19 @@ export default {
|
||||
},
|
||||
emits: ['update:model-value', 'focus', 'submit'],
|
||||
computed: {
|
||||
formatType () {
|
||||
const format = this.field.preferences?.format || ''
|
||||
|
||||
if (/[HhAasz]/.test(format)) return 'datetime'
|
||||
if (format && !/[Dd]/.test(format)) return 'month'
|
||||
|
||||
return 'date'
|
||||
},
|
||||
inputType () {
|
||||
return { datetime: 'datetime-local', month: 'month', date: 'date' }[this.formatType]
|
||||
},
|
||||
dateNowString () {
|
||||
const today = new Date()
|
||||
|
||||
const yyyy = today.getFullYear()
|
||||
const mm = String(today.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(today.getDate()).padStart(2, '0')
|
||||
|
||||
return `${yyyy}-${mm}-${dd}`
|
||||
return this.formatDateValue(new Date())
|
||||
},
|
||||
validationMin () {
|
||||
if (this.field.validation?.min) {
|
||||
@@ -121,6 +132,8 @@ export default {
|
||||
}
|
||||
},
|
||||
withToday () {
|
||||
if (this.formatType === 'datetime') return false
|
||||
|
||||
const todayDate = new Date().setHours(0, 0, 0, 0)
|
||||
|
||||
if (this.validationMin) {
|
||||
@@ -137,9 +150,25 @@ export default {
|
||||
},
|
||||
value: {
|
||||
set (value) {
|
||||
if (this.formatType === 'datetime' && value) {
|
||||
const d = new Date(value)
|
||||
|
||||
if (!isNaN(d)) {
|
||||
this.$emit('update:model-value', d.toISOString())
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('update:model-value', value)
|
||||
},
|
||||
get () {
|
||||
if (this.formatType === 'datetime') {
|
||||
const d = new Date(this.modelValue)
|
||||
|
||||
return isNaN(d) ? '' : this.formatDateValue(d)
|
||||
}
|
||||
|
||||
return this.modelValue
|
||||
}
|
||||
}
|
||||
@@ -163,20 +192,32 @@ export default {
|
||||
|
||||
const parsedDate = new Date(pasteData)
|
||||
|
||||
if (!isNaN(parsedDate)) {
|
||||
const inputEl = this.$refs.input
|
||||
if (isNaN(parsedDate)) return
|
||||
|
||||
inputEl.valueAsDate = new Date(parsedDate.getTime() - parsedDate.getTimezoneOffset() * 60000)
|
||||
|
||||
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
this.setInputValue(parsedDate)
|
||||
},
|
||||
setCurrentDate () {
|
||||
this.setInputValue(new Date())
|
||||
},
|
||||
setInputValue (date) {
|
||||
const inputEl = this.$refs.input
|
||||
|
||||
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
|
||||
if (this.formatType === 'date') {
|
||||
inputEl.valueAsDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
||||
} else {
|
||||
inputEl.value = this.formatDateValue(date)
|
||||
}
|
||||
|
||||
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
},
|
||||
formatDateValue (date) {
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
const ymd = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||
|
||||
if (this.formatType === 'month') return ymd.slice(0, 7)
|
||||
if (this.formatType === 'datetime') return `${ymd}T${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
|
||||
return ymd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
<span
|
||||
v-else-if="field.default_value === '{{date}}'"
|
||||
>
|
||||
{{ t('signing_date') }}
|
||||
{{ /[HhAasz]/.test(field.preferences?.format || '') ? t('signing_date_and_time') : t('signing_date') }}
|
||||
</span>
|
||||
<div
|
||||
v-else-if="field.type === 'cells' && field.default_value"
|
||||
|
||||
@@ -669,6 +669,7 @@ export default {
|
||||
locale: this.locale,
|
||||
baseFetch: this.baseFetch,
|
||||
fieldTypes: this.fieldTypes,
|
||||
dateFormats: this.dateFormats,
|
||||
backgroundColor: this.backgroundColor,
|
||||
withPhone: this.withPhone,
|
||||
withVerification: this.withVerification,
|
||||
@@ -823,6 +824,11 @@ export default {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
dateFormats: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
defaultSubmitters: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -1030,6 +1036,8 @@ export default {
|
||||
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
|
||||
},
|
||||
defaultDateFormat () {
|
||||
if (this.dateFormats.length) return this.dateFormats[0]
|
||||
|
||||
const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US')
|
||||
const isUsTimezone = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
@change="[schema.format = $event.target.value, save()]"
|
||||
>
|
||||
<option
|
||||
v-for="format in dateFormats"
|
||||
v-for="format in availableDateFormats"
|
||||
:key="format"
|
||||
:value="format"
|
||||
>{{ formatDate(new Date(), format) }}</option>
|
||||
@@ -248,7 +248,7 @@ export default {
|
||||
FieldType,
|
||||
IconSettings
|
||||
},
|
||||
inject: ['t', 'save', 'backgroundColor'],
|
||||
inject: ['t', 'save', 'backgroundColor', 'dateFormats'],
|
||||
provide () {
|
||||
return {
|
||||
fieldTypes: ['text', 'number', 'date', 'checkbox', 'radio', 'select']
|
||||
@@ -318,20 +318,23 @@ export default {
|
||||
'space'
|
||||
]
|
||||
},
|
||||
dateFormats () {
|
||||
const formats = [
|
||||
'MM/DD/YYYY',
|
||||
'DD/MM/YYYY',
|
||||
'YYYY-MM-DD',
|
||||
'DD-MM-YYYY',
|
||||
'DD.MM.YYYY',
|
||||
'MMM D, YYYY',
|
||||
'MMMM D, YYYY',
|
||||
'D MMM YYYY',
|
||||
'D MMMM YYYY'
|
||||
]
|
||||
availableDateFormats () {
|
||||
const formats = this.dateFormats.length
|
||||
? [...this.dateFormats]
|
||||
: [
|
||||
'MM/DD/YYYY',
|
||||
'DD/MM/YYYY',
|
||||
'YYYY-MM-DD',
|
||||
'DD-MM-YYYY',
|
||||
'DD.MM.YYYY',
|
||||
'MMM D, YYYY',
|
||||
'MMMM D, YYYY',
|
||||
'MMMM YYYY',
|
||||
'D MMM YYYY',
|
||||
'D MMMM YYYY'
|
||||
]
|
||||
|
||||
if (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko')) {
|
||||
if (!this.dateFormats.length && (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko'))) {
|
||||
formats.push('YYYY년 MM월 DD일')
|
||||
}
|
||||
|
||||
@@ -401,18 +404,47 @@ export default {
|
||||
formatDate (date, format) {
|
||||
const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
|
||||
const dayFormats = { D: 'numeric', DD: '2-digit' }
|
||||
const yearFormats = { YYYY: 'numeric', YY: '2-digit' }
|
||||
const yearFormats = { YYYY: 'numeric', YYY: 'numeric', YY: '2-digit' }
|
||||
const hourFormats = { H: 'numeric', HH: '2-digit', h: 'numeric', hh: '2-digit' }
|
||||
const minuteFormats = { m: 'numeric', mm: '2-digit' }
|
||||
const secondFormats = { s: 'numeric', ss: '2-digit' }
|
||||
|
||||
const parts = new Intl.DateTimeFormat([], {
|
||||
const opts = {
|
||||
day: dayFormats[format.match(/D+/)],
|
||||
month: monthFormats[format.match(/M+/)],
|
||||
year: yearFormats[format.match(/Y+/)]
|
||||
}).formatToParts(date)
|
||||
}
|
||||
|
||||
return format
|
||||
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
||||
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
||||
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
||||
if (format.match(/H+/)) { opts.hour = hourFormats[format.match(/H+/)[0]]; opts.hour12 = false }
|
||||
if (format.match(/h+/)) { opts.hour = hourFormats[format.match(/h+/)[0]]; opts.hour12 = true }
|
||||
if (/[Aa]/.test(format) && opts.hour12 === undefined) opts.hour12 = true
|
||||
if (format.match(/m+/)) opts.minute = minuteFormats[format.match(/m+/)[0]]
|
||||
if (format.match(/s+/)) opts.second = secondFormats[format.match(/s+/)[0]]
|
||||
if (/z/.test(format)) opts.timeZoneName = 'short'
|
||||
|
||||
const partTypes = {
|
||||
M: 'month',
|
||||
D: 'day',
|
||||
Y: 'year',
|
||||
H: 'hour',
|
||||
h: 'hour',
|
||||
m: 'minute',
|
||||
s: 'second',
|
||||
z: 'timeZoneName',
|
||||
A: 'dayPeriod',
|
||||
a: 'dayPeriod'
|
||||
}
|
||||
|
||||
const parts = new Intl.DateTimeFormat([], opts).formatToParts(date)
|
||||
|
||||
return format.replace(/MMMM|MMM|MM|M|DD|D|YYYY|YYY|YY|HH|hh|H|h|mm|m|ss|s|A|a|z/g, (token) => {
|
||||
const value = parts.find((p) => p.type === partTypes[token[0]])?.value
|
||||
|
||||
if (token === 'A') return (value || '').toUpperCase()
|
||||
if (token === 'a') return (value || '').toLowerCase()
|
||||
|
||||
return value
|
||||
})
|
||||
},
|
||||
closeDropdown () {
|
||||
this.$el.getRootNode().activeElement.blur()
|
||||
|
||||
@@ -345,7 +345,7 @@ export default {
|
||||
IconMathFunction,
|
||||
FieldType
|
||||
},
|
||||
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex'],
|
||||
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex', 'dateFormats'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -428,7 +428,8 @@ export default {
|
||||
|
||||
if (this.field.type === 'date') {
|
||||
this.field.preferences.format ||=
|
||||
({ 'de-DE': 'DD.MM.YYYY' }[this.locale] || ((Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') || new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)) ? 'MM/DD/YYYY' : 'DD/MM/YYYY'))
|
||||
this.dateFormats[0] ||
|
||||
({ 'de-DE': 'DD.MM.YYYY' }[this.locale] || ((Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') || new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)) ? 'MM/DD/YYYY' : 'DD/MM/YYYY'))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -514,7 +514,7 @@ export default {
|
||||
ContextSubmenu,
|
||||
ContextModal
|
||||
},
|
||||
inject: ['t', 'getFieldTypeIndex', 'template', 'withCustomFields', 'currencies'],
|
||||
inject: ['t', 'getFieldTypeIndex', 'template', 'withCustomFields', 'currencies', 'dateFormats'],
|
||||
props: {
|
||||
contextMenu: {
|
||||
type: Object,
|
||||
@@ -580,7 +580,7 @@ export default {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldLabels: FieldType.computed.fieldLabels,
|
||||
validationOptions: FieldSettings.computed.validations,
|
||||
dateFormats: FieldSettings.computed.dateFormats,
|
||||
availableDateFormats: FieldSettings.computed.availableDateFormats,
|
||||
numberFormats: FieldSettings.computed.numberFormats,
|
||||
prefillableFieldTypes: FieldSettings.computed.prefillableFieldTypes,
|
||||
verificationMethods: FieldSettings.computed.verificationMethods,
|
||||
@@ -686,7 +686,7 @@ export default {
|
||||
},
|
||||
formatOptions () {
|
||||
switch (this.field.type) {
|
||||
case 'date': return this.dateFormats.map(f => ({ value: f, label: this.formatDate(new Date(), f) }))
|
||||
case 'date': return this.availableDateFormats.map(f => ({ value: f, label: this.formatDate(new Date(), f) }))
|
||||
case 'number': return this.numberFormats.map(f => ({ value: f, label: this.formatNumber(123456789.567, f) }))
|
||||
case 'signature': return this.signatureFormats.map(f => ({ value: f, label: this.t(f) }))
|
||||
default: return []
|
||||
|
||||
@@ -290,7 +290,7 @@
|
||||
@change="$emit('save')"
|
||||
>
|
||||
<option
|
||||
v-for="format in dateFormats"
|
||||
v-for="format in availableDateFormats"
|
||||
:key="format"
|
||||
:value="format"
|
||||
>
|
||||
@@ -610,7 +610,7 @@ export default {
|
||||
IconTypography,
|
||||
IconX
|
||||
},
|
||||
inject: ['template', 't'],
|
||||
inject: ['template', 't', 'dateFormats'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -701,20 +701,23 @@ export default {
|
||||
'space'
|
||||
]
|
||||
},
|
||||
dateFormats () {
|
||||
const formats = [
|
||||
'MM/DD/YYYY',
|
||||
'DD/MM/YYYY',
|
||||
'YYYY-MM-DD',
|
||||
'DD-MM-YYYY',
|
||||
'DD.MM.YYYY',
|
||||
'MMM D, YYYY',
|
||||
'MMMM D, YYYY',
|
||||
'D MMM YYYY',
|
||||
'D MMMM YYYY'
|
||||
]
|
||||
availableDateFormats () {
|
||||
const formats = this.dateFormats.length
|
||||
? [...this.dateFormats]
|
||||
: [
|
||||
'MM/DD/YYYY',
|
||||
'DD/MM/YYYY',
|
||||
'YYYY-MM-DD',
|
||||
'DD-MM-YYYY',
|
||||
'DD.MM.YYYY',
|
||||
'MMM D, YYYY',
|
||||
'MMMM D, YYYY',
|
||||
'MMMM YYYY',
|
||||
'D MMM YYYY',
|
||||
'D MMMM YYYY'
|
||||
]
|
||||
|
||||
if (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko')) {
|
||||
if (!this.dateFormats.length && (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko'))) {
|
||||
formats.push('YYYY년 MM월 DD일')
|
||||
}
|
||||
|
||||
@@ -819,33 +822,49 @@ export default {
|
||||
return pattern?.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups || null
|
||||
},
|
||||
formatDate (date, format) {
|
||||
const monthFormats = {
|
||||
M: 'numeric',
|
||||
MM: '2-digit',
|
||||
MMM: 'short',
|
||||
MMMM: 'long'
|
||||
}
|
||||
const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
|
||||
const dayFormats = { D: 'numeric', DD: '2-digit' }
|
||||
const yearFormats = { YYYY: 'numeric', YYY: 'numeric', YY: '2-digit' }
|
||||
const hourFormats = { H: 'numeric', HH: '2-digit', h: 'numeric', hh: '2-digit' }
|
||||
const minuteFormats = { m: 'numeric', mm: '2-digit' }
|
||||
const secondFormats = { s: 'numeric', ss: '2-digit' }
|
||||
|
||||
const dayFormats = {
|
||||
D: 'numeric',
|
||||
DD: '2-digit'
|
||||
}
|
||||
|
||||
const yearFormats = {
|
||||
YYYY: 'numeric',
|
||||
YY: '2-digit'
|
||||
}
|
||||
|
||||
const parts = new Intl.DateTimeFormat([], {
|
||||
const opts = {
|
||||
day: dayFormats[format.match(/D+/)],
|
||||
month: monthFormats[format.match(/M+/)],
|
||||
year: yearFormats[format.match(/Y+/)]
|
||||
}).formatToParts(date)
|
||||
}
|
||||
|
||||
return format
|
||||
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
||||
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
||||
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
||||
if (format.match(/H+/)) { opts.hour = hourFormats[format.match(/H+/)[0]]; opts.hour12 = false }
|
||||
if (format.match(/h+/)) { opts.hour = hourFormats[format.match(/h+/)[0]]; opts.hour12 = true }
|
||||
if (/[Aa]/.test(format) && opts.hour12 === undefined) opts.hour12 = true
|
||||
if (format.match(/m+/)) opts.minute = minuteFormats[format.match(/m+/)[0]]
|
||||
if (format.match(/s+/)) opts.second = secondFormats[format.match(/s+/)[0]]
|
||||
if (/z/.test(format)) opts.timeZoneName = 'short'
|
||||
|
||||
const partTypes = {
|
||||
M: 'month',
|
||||
D: 'day',
|
||||
Y: 'year',
|
||||
H: 'hour',
|
||||
h: 'hour',
|
||||
m: 'minute',
|
||||
s: 'second',
|
||||
z: 'timeZoneName',
|
||||
A: 'dayPeriod',
|
||||
a: 'dayPeriod'
|
||||
}
|
||||
|
||||
const parts = new Intl.DateTimeFormat([], opts).formatToParts(date)
|
||||
|
||||
return format.replace(/MMMM|MMM|MM|M|DD|D|YYYY|YYY|YY|HH|hh|H|h|mm|m|ss|s|A|a|z/g, (token) => {
|
||||
const value = parts.find((p) => p.type === partTypes[token[0]])?.value
|
||||
|
||||
if (token === 'A') return (value || '').toUpperCase()
|
||||
if (token === 'a') return (value || '').toLowerCase()
|
||||
|
||||
return value
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ const en = {
|
||||
sign_yourself: 'Sign Yourself',
|
||||
set_signing_date: 'Set signing date',
|
||||
signing_date: 'Signing Date',
|
||||
signing_date_and_time: 'Signing Date and Time',
|
||||
send: 'Send',
|
||||
remove: 'Remove',
|
||||
edit: 'Edit',
|
||||
@@ -271,6 +272,7 @@ const es = {
|
||||
with_logo: 'Con logotipo',
|
||||
description: 'Descripción',
|
||||
signing_date: 'Fecha de Firma',
|
||||
signing_date_and_time: 'Fecha y Hora de Firma',
|
||||
display_title: 'Título de visualización',
|
||||
unchecked: 'No marcado',
|
||||
price: 'Precio',
|
||||
@@ -508,6 +510,7 @@ const it = {
|
||||
sign_yourself: 'Firma te stesso',
|
||||
set_signing_date: 'Imposta data di firma',
|
||||
signing_date: 'Data di firma',
|
||||
signing_date_and_time: 'Data e ora di firma',
|
||||
send: 'Invia',
|
||||
remove: 'Rimuovi',
|
||||
edit: 'Modifica',
|
||||
@@ -710,6 +713,7 @@ const pt = {
|
||||
description: 'Descrição',
|
||||
display_title: 'Título de exibição',
|
||||
signing_date: 'Data da Assinatura',
|
||||
signing_date_and_time: 'Data e Hora da Assinatura',
|
||||
unchecked: 'Não marcado',
|
||||
price: 'Preço',
|
||||
equal: 'Igual',
|
||||
@@ -946,6 +950,7 @@ const fr = {
|
||||
sign_yourself: 'Signer vous-même',
|
||||
set_signing_date: 'Définir la date de signature',
|
||||
signing_date: 'Date de signature',
|
||||
signing_date_and_time: 'Date et heure de signature',
|
||||
send: 'Envoyer',
|
||||
remove: 'Supprimer',
|
||||
edit: 'Modifier',
|
||||
@@ -1165,6 +1170,7 @@ const de = {
|
||||
sign_yourself: 'Selbst unterschreiben',
|
||||
set_signing_date: 'Unterzeichnungsdatum festlegen',
|
||||
signing_date: 'Unterzeichnungsdatum',
|
||||
signing_date_and_time: 'Unterzeichnungsdatum und -uhrzeit',
|
||||
send: 'Senden',
|
||||
remove: 'Entfernen',
|
||||
edit: 'Bearbeiten',
|
||||
@@ -1384,6 +1390,7 @@ const nl = {
|
||||
sign_yourself: 'Zelf ondertekenen',
|
||||
set_signing_date: 'Ondertekeningsdatum instellen',
|
||||
signing_date: 'Ondertekeningsdatum',
|
||||
signing_date_and_time: 'Ondertekeningsdatum en -tijd',
|
||||
send: 'Verzenden',
|
||||
remove: 'Verwijderen',
|
||||
edit: 'Bewerken',
|
||||
|
||||
@@ -73,8 +73,8 @@
|
||||
<% elsif field['type'] == 'date' %>
|
||||
<autosize-field></autosize-field>
|
||||
<div class="flex w-full px-0.5 <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
|
||||
<% value = Time.current.in_time_zone(local_assigns[:timezone]).to_date.to_s if value == '{{date}}' %>
|
||||
<div class="w-full"><%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %></div>
|
||||
<% tz = local_assigns[:with_submitter_timezone] ? (submitter.timezone.presence || local_assigns[:timezone]) : local_assigns[:timezone] %>
|
||||
<div class="w-full"><%= value == '{{date}}' ? TimeUtils.format_date_preview(field.dig('preferences', 'format'), local_assigns[:locale], tz) : TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale], timezone: tz) %></div>
|
||||
</div>
|
||||
<% elsif field['type'] == 'number' %>
|
||||
<autosize-field></autosize-field>
|
||||
|
||||
@@ -281,7 +281,7 @@
|
||||
<% if field['type'] == 'number' %>
|
||||
<% value = NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
|
||||
<% elsif field['type'] == 'date' %>
|
||||
<% value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %>
|
||||
<% value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale, timezone: (with_submitter_timezone ? (submitter.timezone.presence || @submission.account.timezone) : @submission.account.timezone)) %>
|
||||
<% end %>
|
||||
<% if (mask = field.dig('preferences', 'mask').presence) %>
|
||||
<% if signed_in? && can?(:read, @submission) %>
|
||||
|
||||
@@ -410,9 +410,7 @@ module Submissions
|
||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||
end
|
||||
|
||||
submitter.values = submitter.values.transform_values do |v|
|
||||
v == '{{date}}' ? Time.current.in_time_zone(submitter.submission.account.timezone).to_date.to_s : v
|
||||
end
|
||||
submitter.values = Submitters::SubmitValues.replace_current_date_placeholders(submitter)
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
@@ -415,7 +415,8 @@ module Submissions
|
||||
composer.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0])
|
||||
else
|
||||
if field['type'] == 'date'
|
||||
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale)
|
||||
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale,
|
||||
timezone:)
|
||||
end
|
||||
|
||||
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'
|
||||
|
||||
@@ -652,7 +652,10 @@ module Submissions
|
||||
end
|
||||
else
|
||||
if field['type'] == 'date'
|
||||
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), locale)
|
||||
timezone = submitter.account.timezone
|
||||
timezone = submitter.timezone || submitter.account.timezone if with_submitter_timezone
|
||||
|
||||
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), locale, timezone:)
|
||||
end
|
||||
|
||||
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'
|
||||
|
||||
@@ -103,17 +103,43 @@ module Submitters
|
||||
end
|
||||
|
||||
def normalize_date(field, value)
|
||||
if value.is_a?(Integer)
|
||||
format = field.dig('preferences', 'format')
|
||||
|
||||
if TimeUtils.format_with_time?(format)
|
||||
normalize_date_time(value, format)
|
||||
elsif TimeUtils.month_only_format?(format)
|
||||
normalize_date_month(value, format)
|
||||
elsif value.is_a?(Integer)
|
||||
Time.zone.at(value.to_s.first(10).to_i).to_date.to_s
|
||||
elsif value.gsub(/\w/, '0') == field.dig('preferences', 'format').to_s.gsub(/\w/, '0')
|
||||
TimeUtils.parse_date_string(value, field.dig('preferences', 'format')).to_s
|
||||
elsif value.gsub(/\w/, '0') == format.to_s.gsub(/\w/, '0')
|
||||
TimeUtils.parse_date_string(value, format).to_s
|
||||
else
|
||||
Date.parse(value).to_s
|
||||
end
|
||||
rescue Date::Error
|
||||
rescue ArgumentError
|
||||
value
|
||||
end
|
||||
|
||||
def normalize_date_time(value, format)
|
||||
if value.is_a?(Integer)
|
||||
Time.zone.at(value.to_s.first(10).to_i).utc.iso8601
|
||||
elsif value.to_s.match?(/T\d{2}:\d{2}/)
|
||||
Time.iso8601(value).utc.iso8601
|
||||
else
|
||||
TimeUtils.parse_date_string(value, format).utc.iso8601
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_date_month(value, format)
|
||||
if value.is_a?(Integer)
|
||||
Time.zone.at(value.to_s.first(10).to_i).strftime('%Y-%m')
|
||||
elsif value.to_s.match?(/\A\d{4}-\d{2}\z/)
|
||||
value
|
||||
else
|
||||
TimeUtils.parse_date_string(value, format).strftime('%Y-%m')
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_fields(template, submitter_name: nil, for_submitter: nil)
|
||||
if submitter_name && !for_submitter
|
||||
submitter =
|
||||
|
||||
@@ -86,9 +86,7 @@ module Submitters
|
||||
submitter.values = maybe_remove_condition_values(submitter, required_field_uuids_acc:)
|
||||
end
|
||||
|
||||
submitter.values = submitter.values.transform_values do |v|
|
||||
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
|
||||
end
|
||||
submitter.values = replace_current_date_placeholders(submitter)
|
||||
|
||||
required_field_uuids_acc.each do |uuid|
|
||||
next if submitter.values[uuid].present?
|
||||
@@ -194,7 +192,7 @@ module Submitters
|
||||
|
||||
next if value.blank?
|
||||
|
||||
acc[field['uuid']] = template_default_value_for_submitter(value, submitter, with_time: true)
|
||||
acc[field['uuid']] = template_default_value_for_submitter(value, submitter, field:, with_time: true)
|
||||
end
|
||||
|
||||
default_values.compact_blank.merge(submitter.values)
|
||||
@@ -248,7 +246,20 @@ module Submitters
|
||||
0
|
||||
end
|
||||
|
||||
def template_default_value_for_submitter(value, submitter, with_time: false)
|
||||
def replace_current_date_placeholders(submitter)
|
||||
submitter.values.each_with_object({}) do |(uuid, v), acc|
|
||||
acc[uuid] =
|
||||
if v == '{{date}}'
|
||||
field = submitter.submission.fields_uuid_index[uuid]
|
||||
|
||||
TimeUtils.current_date_value(field&.dig('preferences', 'format'), submitter.account.timezone)
|
||||
else
|
||||
v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def template_default_value_for_submitter(value, submitter, with_time: false, field: nil)
|
||||
return if value.blank?
|
||||
return if submitter.blank?
|
||||
|
||||
@@ -257,7 +268,8 @@ module Submitters
|
||||
replace_default_variables(value,
|
||||
submitter.attributes.merge('role' => role),
|
||||
submitter.submission,
|
||||
with_time:)
|
||||
with_time:,
|
||||
field:)
|
||||
end
|
||||
|
||||
def maybe_remove_condition_values(submitter, required_field_uuids_acc: nil)
|
||||
@@ -394,7 +406,7 @@ module Submitters
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
|
||||
def replace_default_variables(value, attrs, submission, with_time: false)
|
||||
def replace_default_variables(value, attrs, submission, with_time: false, field: nil)
|
||||
return value if value.in?([true, false]) || value.is_a?(Numeric) || value.is_a?(Array)
|
||||
return if value.blank?
|
||||
|
||||
@@ -412,7 +424,7 @@ module Submitters
|
||||
when 'hour', 'minute', 'day', 'month', 'year'
|
||||
with_time ? Time.current.in_time_zone(submission.account.timezone).strftime(STRFTIME_MAP[key]) : e
|
||||
when 'date'
|
||||
with_time ? Time.current.in_time_zone(submission.account.timezone).to_date.to_s : e
|
||||
with_time ? TimeUtils.current_date_value(field&.dig('preferences', 'format'), submission.account.timezone) : e
|
||||
when 'role', 'email', 'phone', 'name'
|
||||
attrs[key] || e
|
||||
else
|
||||
|
||||
+88
-14
@@ -19,6 +19,47 @@ module TimeUtils
|
||||
'YY' => '%y'
|
||||
}.freeze
|
||||
|
||||
HOUR_FORMATS = {
|
||||
'H' => '%-H',
|
||||
'HH' => '%H',
|
||||
'h' => '%-I',
|
||||
'hh' => '%I'
|
||||
}.freeze
|
||||
|
||||
MINUTE_FORMATS = {
|
||||
'm' => '%-M',
|
||||
'mm' => '%M'
|
||||
}.freeze
|
||||
|
||||
SECOND_FORMATS = {
|
||||
's' => '%-S',
|
||||
'ss' => '%S'
|
||||
}.freeze
|
||||
|
||||
AMPM_FORMATS = {
|
||||
'A' => '%p',
|
||||
'a' => '%P'
|
||||
}.freeze
|
||||
|
||||
TIMEZONE_FORMATS = {
|
||||
'z' => '%Z'
|
||||
}.freeze
|
||||
|
||||
TIME_FORMATS = HOUR_FORMATS.merge(MINUTE_FORMATS)
|
||||
.merge(SECOND_FORMATS)
|
||||
.merge(AMPM_FORMATS)
|
||||
.freeze
|
||||
|
||||
ALL_FORMATS = MONTH_FORMATS.merge(DAY_FORMATS)
|
||||
.merge(YEAR_FORMATS)
|
||||
.merge(TIME_FORMATS)
|
||||
.merge(TIMEZONE_FORMATS)
|
||||
.freeze
|
||||
|
||||
TOKEN_REGEX = /MMMM|MMM|MM|M|DD|D|YYYY|YYY|YY|HH|hh|H|h|mm|m|ss|s|A|a|z/
|
||||
|
||||
MONTH_ONLY_VALUE_REGEX = /\A\d{4}-\d{2}\z/
|
||||
|
||||
DEFAULT_DATE_FORMAT_US = 'MM/DD/YYYY'
|
||||
DEFAULT_DATE_FORMAT = 'DD/MM/YYYY'
|
||||
|
||||
@@ -42,27 +83,60 @@ module TimeUtils
|
||||
end
|
||||
end
|
||||
|
||||
def parse_date_string(string, pattern)
|
||||
pattern = pattern.sub(/Y+/, YEAR_FORMATS)
|
||||
.sub(/M+/, MONTH_FORMATS)
|
||||
.sub(/D+/, DAY_FORMATS)
|
||||
|
||||
Date.strptime(string, pattern)
|
||||
def format_with_time?(format)
|
||||
format.to_s.match?(/[HhAasz]/)
|
||||
end
|
||||
|
||||
def format_date_string(string, format, locale)
|
||||
date = Date.parse(string.to_s)
|
||||
def month_only_format?(format)
|
||||
format.to_s.present? && !format.to_s.match?(/[DdHhAasz]/)
|
||||
end
|
||||
|
||||
format = format.upcase if format
|
||||
def format_date_preview(format, locale, timezone)
|
||||
return '' if format.blank?
|
||||
|
||||
format = format.upcase unless format_with_time?(format)
|
||||
preview_pattern = format.gsub(TOKEN_REGEX) { |token| TIME_FORMATS.key?(token) ? '--' : ALL_FORMATS[token] }
|
||||
|
||||
I18n.l(Time.current.in_time_zone(timezone.presence || Time.zone.name), format: preview_pattern, locale:)
|
||||
end
|
||||
|
||||
def current_date_value(format, timezone)
|
||||
tz = timezone.presence || Time.zone.name
|
||||
|
||||
if format_with_time?(format)
|
||||
Time.current.utc.iso8601
|
||||
elsif month_only_format?(format)
|
||||
Time.current.in_time_zone(tz).strftime('%Y-%m')
|
||||
else
|
||||
Time.current.in_time_zone(tz).to_date.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def parse_date_string(string, pattern)
|
||||
with_time = format_with_time?(pattern)
|
||||
pattern = pattern.upcase unless with_time
|
||||
pattern = pattern.gsub(TOKEN_REGEX, ALL_FORMATS)
|
||||
|
||||
with_time ? Time.zone.strptime(string, pattern) : Date.strptime(string, pattern)
|
||||
end
|
||||
|
||||
def format_date_string(string, format, locale, timezone: nil)
|
||||
format = format.upcase if format && !format_with_time?(format)
|
||||
format ||= locale.to_s.ends_with?('US') ? DEFAULT_DATE_FORMAT_US : DEFAULT_DATE_FORMAT
|
||||
|
||||
i18n_format = format.sub(/D+/) { DAY_FORMATS[format[/D+/]] }
|
||||
.sub(/M+/) { MONTH_FORMATS[format[/M+/]] }
|
||||
.sub(/Y+/) { YEAR_FORMATS[format[/Y+/]] }
|
||||
date =
|
||||
if format_with_time?(format)
|
||||
Time.iso8601(string.to_s).in_time_zone(timezone.presence || Time.zone.name)
|
||||
elsif string.to_s.match?(MONTH_ONLY_VALUE_REGEX)
|
||||
year, month = string.to_s.split('-').map(&:to_i)
|
||||
|
||||
I18n.l(date, format: i18n_format, locale:)
|
||||
rescue Date::Error
|
||||
Date.new(year, month, 1)
|
||||
else
|
||||
Date.parse(string.to_s)
|
||||
end
|
||||
|
||||
I18n.l(date, format: format.gsub(TOKEN_REGEX, ALL_FORMATS), locale:)
|
||||
rescue ArgumentError
|
||||
string
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user