input mode condition and formula
This commit is contained in:
@@ -113,7 +113,7 @@
|
|||||||
<div
|
<div
|
||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
class="flex items-center px-0.5"
|
class="flex items-center px-0.5"
|
||||||
:style="{ color: field.preferences?.color }"
|
:style="{ color: isConditionMatch ? field.preferences?.color : '#9ca3af' }"
|
||||||
:class="{ 'w-full h-full': isWFullType }"
|
:class="{ 'w-full h-full': isWFullType }"
|
||||||
>
|
>
|
||||||
<IconCheck
|
<IconCheck
|
||||||
@@ -131,9 +131,9 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<span
|
<span
|
||||||
v-else-if="field.type === 'number' && !isValueInput && (field.default_value || field.default_value == 0)"
|
v-else-if="field.type === 'number' && !isContenteditable && (displayValue || displayValue == 0)"
|
||||||
class="whitespace-pre-wrap"
|
class="whitespace-pre-wrap"
|
||||||
>{{ formatNumber(field.default_value, field.preferences?.format) }}</span>
|
>{{ formatNumber(displayValue, field.preferences?.format) }}</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="field.default_value === '{{date}}'"
|
v-else-if="field.default_value === '{{date}}'"
|
||||||
>
|
>
|
||||||
@@ -183,12 +183,12 @@
|
|||||||
:contenteditable="isValueInput"
|
:contenteditable="isValueInput"
|
||||||
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-base-content/30"
|
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-base-content/30"
|
||||||
:class="{ 'cursor-text': isValueInput }"
|
:class="{ 'cursor-text': isValueInput }"
|
||||||
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))"
|
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (isConditionMatch ? (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value')) : '')"
|
||||||
@blur="onDefaultValueBlur"
|
@blur="onDefaultValueBlur"
|
||||||
@focus="selectedAreasRef.value = [area]"
|
@focus="selectedAreasRef.value = [area]"
|
||||||
@paste.prevent="onPaste"
|
@paste.prevent="onPaste"
|
||||||
@keydown.enter="onDefaultValueEnter"
|
@keydown.enter="onDefaultValueEnter"
|
||||||
>{{ field.default_value }}</span>
|
>{{ displayValue }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<component
|
<component
|
||||||
@@ -241,6 +241,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
conditionalFieldIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
formulaValuesIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
isDraw: {
|
isDraw: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -323,11 +333,27 @@ export default {
|
|||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldLabels: FieldType.computed.fieldLabels,
|
fieldLabels: FieldType.computed.fieldLabels,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
isConditionMatch () {
|
||||||
|
return !this.inputMode || this.conditionalFieldIndex[this.field.uuid] !== false
|
||||||
|
},
|
||||||
|
displayValue () {
|
||||||
|
if (this.field.preferences?.formula && this.field.type !== 'payment') {
|
||||||
|
const computed = this.formulaValuesIndex[this.field.uuid]
|
||||||
|
|
||||||
|
if (computed != null) {
|
||||||
|
return computed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.field.default_value
|
||||||
|
},
|
||||||
bgClasses () {
|
bgClasses () {
|
||||||
if (this.field.type === 'heading') {
|
if (this.field.type === 'heading') {
|
||||||
return 'bg-gray-50'
|
return 'bg-gray-50'
|
||||||
} else if (this.field.type === 'strikethrough') {
|
} else if (this.field.type === 'strikethrough') {
|
||||||
return 'bg-transparent'
|
return 'bg-transparent'
|
||||||
|
} else if (!this.isConditionMatch) {
|
||||||
|
return 'bg-gray-100'
|
||||||
} else {
|
} else {
|
||||||
return this.bgColors[this.submitterIndex % this.bgColors.length]
|
return this.bgColors[this.submitterIndex % this.bgColors.length]
|
||||||
}
|
}
|
||||||
@@ -337,6 +363,8 @@ export default {
|
|||||||
return ''
|
return ''
|
||||||
} else if (this.field.type === 'strikethrough') {
|
} else if (this.field.type === 'strikethrough') {
|
||||||
return 'border-dashed border-gray-300'
|
return 'border-dashed border-gray-300'
|
||||||
|
} else if (!this.isConditionMatch) {
|
||||||
|
return 'border-gray-300'
|
||||||
} else {
|
} else {
|
||||||
return this.borderColors[this.submitterIndex % this.borderColors.length]
|
return this.borderColors[this.submitterIndex % this.borderColors.length]
|
||||||
}
|
}
|
||||||
@@ -390,7 +418,7 @@ export default {
|
|||||||
return this.basePageWidth / 612.0
|
return this.basePageWidth / 612.0
|
||||||
},
|
},
|
||||||
isDefaultValuePresent () {
|
isDefaultValuePresent () {
|
||||||
return this.field?.default_value || this.field?.default_value === 0
|
return this.field?.default_value || this.field?.default_value === 0 || this.displayValue || this.displayValue === 0
|
||||||
},
|
},
|
||||||
isSelectInput () {
|
isSelectInput () {
|
||||||
return this.inputMode && (this.field.type === 'select' || (this.field.type === 'radio' && this.field.areas?.length < 2))
|
return this.inputMode && (this.field.type === 'select' || (this.field.type === 'radio' && this.field.areas?.length < 2))
|
||||||
@@ -399,6 +427,8 @@ export default {
|
|||||||
return this.inputMode && (this.field.type === 'checkbox' || (['radio', 'multiple'].includes(this.field.type) && this.area.option_uuid))
|
return this.inputMode && (this.field.type === 'checkbox' || (['radio', 'multiple'].includes(this.field.type) && this.area.option_uuid))
|
||||||
},
|
},
|
||||||
isValueInput () {
|
isValueInput () {
|
||||||
|
if (this.inputMode && this.field.preferences?.formula) return false
|
||||||
|
|
||||||
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable ||
|
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable ||
|
||||||
(this.inputMode && (['text', 'number'].includes(this.field.type) || (this.field.type === 'date' && this.field.default_value !== '{{date}}')))
|
(this.inputMode && (['text', 'number'].includes(this.field.type) || (this.field.type === 'date' && this.field.default_value !== '{{date}}')))
|
||||||
},
|
},
|
||||||
@@ -511,7 +541,7 @@ export default {
|
|||||||
return option?.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
|
return option?.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
|
||||||
},
|
},
|
||||||
maybeToggleDefaultValue () {
|
maybeToggleDefaultValue () {
|
||||||
if (!this.editable || this.isCmdKeyRef.value) {
|
if (!this.editable || this.isCmdKeyRef.value || this.field.preferences?.formula) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,6 +589,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
focusValueInput (e) {
|
focusValueInput (e) {
|
||||||
|
if (this.inputMode && this.field.type === 'number' && !this.isContenteditable && !this.field.preferences?.formula) {
|
||||||
|
this.isContenteditable = true
|
||||||
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.defaultValue && this.$refs.defaultValue !== document.activeElement) {
|
if (this.$refs.defaultValue && this.$refs.defaultValue !== document.activeElement) {
|
||||||
this.$refs.defaultValue.focus()
|
this.$refs.defaultValue.focus()
|
||||||
@@ -624,6 +658,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDefaultValueBlur (e) {
|
onDefaultValueBlur (e) {
|
||||||
|
if (this.field.preferences?.formula) {
|
||||||
|
this.isContenteditable = false
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const text = this.$refs.defaultValue.innerText.trim()
|
const text = this.$refs.defaultValue.innerText.trim()
|
||||||
|
|
||||||
this.isContenteditable = false
|
this.isContenteditable = false
|
||||||
|
|||||||
@@ -381,6 +381,8 @@
|
|||||||
:document="document"
|
:document="document"
|
||||||
:is-drag="!!dragField"
|
:is-drag="!!dragField"
|
||||||
:input-mode="inputMode"
|
:input-mode="inputMode"
|
||||||
|
:conditional-field-index="conditionalFieldIndex"
|
||||||
|
:formula-values-index="formulaValuesIndex"
|
||||||
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||||
:allow-draw="!onlyDefinedFields || drawField || drawCustomField"
|
:allow-draw="!onlyDefinedFields || drawField || drawCustomField"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
@@ -619,6 +621,16 @@ import { v4 } from 'uuid'
|
|||||||
import { ref, computed, toRaw, defineAsyncComponent } from 'vue'
|
import { ref, computed, toRaw, defineAsyncComponent } from 'vue'
|
||||||
import * as i18n from './i18n'
|
import * as i18n from './i18n'
|
||||||
|
|
||||||
|
const isEmpty = (obj) => {
|
||||||
|
if (obj == null) return true
|
||||||
|
if (Array.isArray(obj)) return obj.length === 0
|
||||||
|
if (typeof obj === 'string') return obj.trim().length === 0
|
||||||
|
if (typeof obj === 'object') return Object.keys(obj).length === 0
|
||||||
|
if (obj === false) return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateBuilder',
|
name: 'TemplateBuilder',
|
||||||
components: {
|
components: {
|
||||||
@@ -971,7 +983,8 @@ export default {
|
|||||||
drawCustomField: null,
|
drawCustomField: null,
|
||||||
drawOption: null,
|
drawOption: null,
|
||||||
dragField: null,
|
dragField: null,
|
||||||
isDragFile: false
|
isDragFile: false,
|
||||||
|
isMathLoaded: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -1052,6 +1065,43 @@ export default {
|
|||||||
|
|
||||||
return map
|
return map
|
||||||
},
|
},
|
||||||
|
fieldsUuidIndex () {
|
||||||
|
return this.template.fields.reduce((acc, f) => {
|
||||||
|
acc[f.uuid] = f
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
conditionalFieldIndex () {
|
||||||
|
if (!this.inputMode) return {}
|
||||||
|
|
||||||
|
const cache = {}
|
||||||
|
|
||||||
|
return this.template.fields.reduce((acc, f) => {
|
||||||
|
acc[f.uuid] = this.checkFieldConditions(f, cache)
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
formulaValuesIndex () {
|
||||||
|
const formulaFields = this.template.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.hasFormulaDependencyValue(f))
|
||||||
|
|
||||||
|
if (!formulaFields.length) return {}
|
||||||
|
|
||||||
|
if (!this.isMathLoaded) {
|
||||||
|
this.loadCalculator()
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formulaFields.reduce((acc, f) => {
|
||||||
|
if (this.conditionalFieldIndex[f.uuid] !== false) {
|
||||||
|
acc[f.uuid] = this.calculateFormula(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
isAllRequiredFieldsAdded () {
|
isAllRequiredFieldsAdded () {
|
||||||
return !this.defaultRequiredFields?.some((f) => {
|
return !this.defaultRequiredFields?.some((f) => {
|
||||||
return !this.template.fields?.some((field) => field.name === f.name)
|
return !this.template.fields?.some((field) => field.name === f.name)
|
||||||
@@ -1190,6 +1240,130 @@ export default {
|
|||||||
toRaw,
|
toRaw,
|
||||||
applyCustomFieldAttributes: Fields.methods.applyCustomFieldAttributes,
|
applyCustomFieldAttributes: Fields.methods.applyCustomFieldAttributes,
|
||||||
buildExistingFields: Fields.methods.buildExistingFields,
|
buildExistingFields: Fields.methods.buildExistingFields,
|
||||||
|
async loadCalculator () {
|
||||||
|
if (this.math) return
|
||||||
|
|
||||||
|
const { Calculator } = await import('../submission_form/calculator')
|
||||||
|
|
||||||
|
this.math = new Calculator()
|
||||||
|
this.isMathLoaded = true
|
||||||
|
},
|
||||||
|
optionValue (option, index) {
|
||||||
|
if (option.value) {
|
||||||
|
return option.value
|
||||||
|
} else {
|
||||||
|
return `${this.t('option')} ${index + 1}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkFieldConditions (field, cache = {}) {
|
||||||
|
const cacheKey = field.uuid || field.attachment_uuid
|
||||||
|
|
||||||
|
if (cache[cacheKey] !== undefined) {
|
||||||
|
return cache[cacheKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.conditions?.length) {
|
||||||
|
const result = field.conditions.reduce((acc, cond) => {
|
||||||
|
if (cond.operation === 'or') {
|
||||||
|
acc.push(acc.pop() || this.checkFieldCondition(cond, cache))
|
||||||
|
} else {
|
||||||
|
acc.push(this.checkFieldCondition(cond, cache))
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
cache[cacheKey] = !result.includes(false)
|
||||||
|
} else {
|
||||||
|
cache[cacheKey] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache[cacheKey]
|
||||||
|
},
|
||||||
|
checkFieldCondition (condition, cache = {}) {
|
||||||
|
const field = this.fieldsUuidIndex[condition.field_uuid]
|
||||||
|
|
||||||
|
if (['not_empty', 'checked', 'equal', 'contains', 'greater_than', 'less_than'].includes(condition.action) && field && !this.checkFieldConditions(field, cache)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValue = !field || isEmpty(field.default_value) ? null : field.default_value
|
||||||
|
|
||||||
|
if (['empty', 'unchecked'].includes(condition.action)) {
|
||||||
|
return isEmpty(defaultValue)
|
||||||
|
} else if (['not_empty', 'checked'].includes(condition.action)) {
|
||||||
|
return !isEmpty(defaultValue)
|
||||||
|
} else if (field?.type === 'number' && ['equal', 'not_equal', 'greater_than', 'less_than'].includes(condition.action)) {
|
||||||
|
const value = defaultValue
|
||||||
|
|
||||||
|
if (isEmpty(value) || isEmpty(condition.value)) return false
|
||||||
|
|
||||||
|
const actual = parseFloat(value)
|
||||||
|
const expected = parseFloat(condition.value)
|
||||||
|
|
||||||
|
if (Number.isNaN(actual) || Number.isNaN(expected)) return false
|
||||||
|
|
||||||
|
if (condition.action === 'equal') return Math.abs(actual - expected) < Number.EPSILON
|
||||||
|
if (condition.action === 'not_equal') return Math.abs(actual - expected) > Number.EPSILON
|
||||||
|
if (condition.action === 'greater_than') return actual > expected
|
||||||
|
if (condition.action === 'less_than') return actual < expected
|
||||||
|
|
||||||
|
return false
|
||||||
|
} else if (['equal', 'contains'].includes(condition.action) && field) {
|
||||||
|
if (field.options) {
|
||||||
|
const option = field.options.find((o) => o.uuid === condition.value)
|
||||||
|
|
||||||
|
if (option) {
|
||||||
|
const values = [defaultValue].flat()
|
||||||
|
|
||||||
|
return values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return [defaultValue].flat().includes(condition.value)
|
||||||
|
}
|
||||||
|
} else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) {
|
||||||
|
if (field.options) {
|
||||||
|
const option = field.options.find((o) => o.uuid === condition.value)
|
||||||
|
|
||||||
|
if (option) {
|
||||||
|
const values = [defaultValue].flat()
|
||||||
|
|
||||||
|
return !values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
normalizeFormula (formula, depth = 0) {
|
||||||
|
if (depth > 10) return formula
|
||||||
|
|
||||||
|
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
|
if (this.fieldsUuidIndex[uuid]?.preferences?.formula) {
|
||||||
|
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
||||||
|
} else {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
calculateFormula (field) {
|
||||||
|
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
|
return this.fieldsUuidIndex[uuid]?.default_value || 0.0
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.math.evaluate(transformedFormula.toLowerCase())
|
||||||
|
},
|
||||||
|
hasFormulaDependencyValue (field) {
|
||||||
|
const normalized = this.normalizeFormula(field.preferences.formula)
|
||||||
|
|
||||||
|
return [...normalized.matchAll(/{{(.*?)}}/g)].some(([, uuid]) => !isEmpty(this.fieldsUuidIndex[uuid]?.default_value))
|
||||||
|
},
|
||||||
addCustomField (field) {
|
addCustomField (field) {
|
||||||
return this.$refs.fields.addCustomField(field)
|
return this.$refs.fields.addCustomField(field)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
:key="image.id"
|
:key="image.id"
|
||||||
:ref="setPageRefs"
|
:ref="setPageRefs"
|
||||||
:input-mode="inputMode"
|
:input-mode="inputMode"
|
||||||
|
:conditional-field-index="conditionalFieldIndex"
|
||||||
|
:formula-values-index="formulaValuesIndex"
|
||||||
:number="index"
|
:number="index"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:data-page="index"
|
:data-page="index"
|
||||||
@@ -64,6 +66,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
conditionalFieldIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
formulaValuesIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
areasIndex: {
|
areasIndex: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -37,6 +37,8 @@
|
|||||||
:ref="setAreaRefs"
|
:ref="setAreaRefs"
|
||||||
:area="item.area"
|
:area="item.area"
|
||||||
:input-mode="inputMode"
|
:input-mode="inputMode"
|
||||||
|
:conditional-field-index="conditionalFieldIndex"
|
||||||
|
:formula-values-index="formulaValuesIndex"
|
||||||
:page-width="width"
|
:page-width="width"
|
||||||
:page-height="height"
|
:page-height="height"
|
||||||
:field="item.field"
|
:field="item.field"
|
||||||
@@ -180,6 +182,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
conditionalFieldIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
formulaValuesIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
defaultFields: {
|
defaultFields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user