add formula placeholder

This commit is contained in:
Pete Matsyburka
2024-02-24 01:01:36 +02:00
parent beb677734f
commit 8227dd4f6a
13 changed files with 495 additions and 21 deletions
+1
View File
@@ -90,6 +90,7 @@ window.customElements.define('template-builder', class extends HTMLElement {
withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false',
withPayment: this.dataset.withPayment === 'true',
withFormula: this.dataset.withFormula === 'true',
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes,
isDirectUpload: this.dataset.isDirectUpload === 'true'
+11 -1
View File
@@ -8,6 +8,11 @@
:current-step="currentStepFields"
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
/>
<FormulaFieldAreas
v-if="formulaFields.length"
:fields="formulaFields"
:values="values"
/>
<button
v-if="!isFormVisible"
id="expand_form_button"
@@ -396,6 +401,7 @@
<script>
import FieldAreas from './areas'
import FormulaFieldAreas from './formula_areas'
import ImageStep from './image_step'
import SignatureStep from './signature_step'
import InitialsStep from './initials_step'
@@ -444,6 +450,7 @@ export default {
IconArrowsDiagonal,
TextStep,
NumberStep,
FormulaFieldAreas,
PhoneStep,
PaymentStep,
IconArrowsDiagonalMinimize2,
@@ -636,6 +643,9 @@ export default {
return acc
}, [])
},
formulaFields () {
return this.fields.filter((f) => f.preferences?.formula)
},
attachmentsIndex () {
return this.attachments.reduce((acc, a) => {
acc[a.uuid] = a
@@ -785,7 +795,7 @@ export default {
this.scrollIntoField(step[0])
}
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
this.$refs.form.querySelector('input[type="date"], input[type="number"], input[type="text"], select')?.focus()
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
this.$refs.form.querySelector('input[type="file"]')?.click()
@@ -0,0 +1,97 @@
<template>
<template
v-for="(field, fieldIndex) in fields"
:key="field.uuid"
>
<template
v-for="(area, areaIndex) in field.areas"
:key="areaIndex"
>
<Teleport
v-if="findPageElementForArea(area)"
:to="findPageElementForArea(area)"
>
<FieldArea
v-if="isMathLoaded"
:model-value="calculateFormula(field)"
:field="field"
:area="area"
:submittable="false"
:field-index="fieldIndex"
/>
</Teleport>
</template>
</template>
</template>
<script>
import FieldArea from './area'
export default {
name: 'FormulaFieldAreas',
components: {
FieldArea
},
props: {
fields: {
type: Array,
required: false,
default: () => []
},
values: {
type: Object,
required: false,
default: () => ({})
}
},
data () {
return {
isMathLoaded: false
}
},
async mounted () {
const {
create,
evaluateDependencies,
addDependencies,
subtractDependencies,
divideDependencies,
multiplyDependencies,
powDependencies,
roundDependencies,
absDependencies,
sinDependencies,
tanDependencies,
cosDependencies
} = await import('mathjs')
this.math = create({
evaluateDependencies,
addDependencies,
subtractDependencies,
divideDependencies,
multiplyDependencies,
powDependencies,
roundDependencies,
absDependencies,
sinDependencies,
tanDependencies,
cosDependencies
})
this.isMathLoaded = true
},
methods: {
findPageElementForArea (area) {
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
},
calculateFormula (field) {
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
return this.values[uuid] || 0.0
})
return this.math.evaluate(transformedFormula.toLowerCase())
}
}
}
</script>
@@ -59,7 +59,7 @@ export default {
default: true
},
modelValue: {
type: String,
type: [String, Number],
required: false,
default: ''
}
+4 -1
View File
@@ -206,9 +206,11 @@ export default {
}
},
computed: {
defaultName: Field.computed.defaultName,
fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons,
defaultName () {
return this.buildDefaultName(this.field, this.template.fields)
},
optionIndexText () {
if (this.area.option_uuid && this.field.options) {
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
@@ -312,6 +314,7 @@ export default {
}
},
methods: {
buildDefaultName: Field.methods.buildDefaultName,
onNameFocus (e) {
this.selectedAreaRef.value = this.area
@@ -273,6 +273,7 @@
@select="startFieldDraw($event)"
/>
</div>
<div id="docuseal_modal_container" />
</div>
</template>
@@ -321,6 +322,7 @@ export default {
backgroundColor: this.backgroundColor,
withPhone: this.withPhone,
withPayment: this.withPayment,
withFormula: this.withFormula,
defaultDrawFieldType: this.defaultDrawFieldType,
selectedAreaRef: computed(() => this.selectedAreaRef)
}
@@ -449,6 +451,11 @@ export default {
required: false,
default: false
},
withFormula: {
type: Boolean,
required: false,
default: false
},
onlyDefinedFields: {
type: Boolean,
required: false,
+69 -18
View File
@@ -65,6 +65,17 @@
:stroke-width="1.6"
/>
</button>
<button
v-if="field.preferences?.formula"
class="relative cursor-pointer text-transparent group-hover:text-base-content"
:disabled="!withFormula"
@click="isShowFormulaModal = true"
>
<IconMathFunction
:width="18"
:stroke-width="1.6"
/>
</button>
<PaymentSettings
v-if="field.type === 'payment'"
:field="field"
@@ -193,6 +204,26 @@
<span class="label-text">{{ t('required') }}</span>
</label>
</li>
<li
v-if="field.type == 'number'"
:class="{'tooltip tooltip-bottom': !withFormula}"
:data-tip="withFormula ? '' : 'Available in Pro'"
@click.stop
>
<label
class="label-text cursor-pointer py-1.5 text-center w-full flex items-center"
@click="isShowFormulaModal = withFormula"
>
<IconMathFunction
width="18px"
height="18px"
class="ml-0.5 mr-1"
/>
<span class="text-sm">
{{ t('formula') }}
</span>
</label>
</li>
<li
v-if="field.type == 'checkbox'"
@click.stop
@@ -359,6 +390,16 @@
</button>
</div>
</div>
<Teleport
v-if="isShowFormulaModal"
:to="modalContainerEl"
>
<FormulaModal
:field="field"
:build-default-name="buildDefaultName"
@close="isShowFormulaModal = false"
/>
</Teleport>
</div>
</template>
@@ -366,7 +407,8 @@
import Contenteditable from './contenteditable'
import FieldType from './field_type'
import PaymentSettings from './payment_settings'
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
import FormulaModal from './formula_modal'
import { IconMathFunction, IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
import { v4 } from 'uuid'
export default {
@@ -377,11 +419,13 @@ export default {
IconShape,
PaymentSettings,
IconNewSection,
FormulaModal,
IconTrashX,
IconMathFunction,
IconCopy,
FieldType
},
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't'],
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't', 'withFormula'],
props: {
field: {
type: Object,
@@ -403,11 +447,15 @@ export default {
return {
isNameFocus: false,
showPaymentModal: false,
isShowFormulaModal: false,
renderDropdown: false
}
},
computed: {
fieldNames: FieldType.computed.fieldNames,
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
},
dateFormats () {
return [
'MM/DD/YYYY',
@@ -422,22 +470,7 @@ export default {
]
},
defaultName () {
if (this.field.type === 'payment' && this.field.preferences?.price) {
const { price, currency } = this.field.preferences || {}
const formattedPrice = new Intl.NumberFormat([], {
style: 'currency',
currency
}).format(price)
return `${this.fieldNames[this.field.type]} ${formattedPrice}`
} else {
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
const suffix = { multiple: this.t('select'), radio: this.t('group') }[this.field.type] || this.t('field')
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
}
return this.buildDefaultName(this.field, this.template.fields)
},
areas () {
return this.field.areas || []
@@ -452,6 +485,24 @@ export default {
}
},
methods: {
buildDefaultName (field, fields) {
if (field.type === 'payment' && field.preferences?.price) {
const { price, currency } = field.preferences || {}
const formattedPrice = new Intl.NumberFormat([], {
style: 'currency',
currency
}).format(price)
return `${this.fieldNames[field.type]} ${formattedPrice}`
} else {
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
const suffix = { multiple: this.t('select'), radio: this.t('group') }[field.type] || this.t('field')
return `${this.fieldNames[field.type]} ${suffix} ${typeIndex + 1}`
}
},
formatDate (date, format) {
const monthFormats = {
M: 'numeric',
@@ -0,0 +1,216 @@
<template>
<div
class="modal modal-open items-start !animate-none overflow-y-auto"
>
<div
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span>
{{ t('formula') }}
</span>
<a
href="#"
class="text-xl"
@click.prevent="$emit('close')"
>&times;</a>
</div>
<div>
<div class="flex-inline mb-2 gap-2 space-y-1">
<button
v-for="f in fields"
:key="f.uuid"
class="mr-1 btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f, template.fields)}}}`)"
>
<IconCodePlus
width="20"
height="20"
stroke-width="1.5"
/>
{{ f.name || buildDefaultName(f, template.fields) }}
</button>
</div>
<div>
<div class="flex">
<textarea
ref="textarea"
v-model="formula"
class="base-textarea !rounded-xl !text-base font-mono w-full !outline-0 !ring-0 !px-3"
required="true"
@input="resizeTextarea"
/>
</div>
<div class="mb-3 mt-1">
<div
target="blank"
class="text-sm mb-2 inline space-x-2"
>
<button
class="bg-base-200 px-2 rounded-xl"
@click="insertTextUnderCursor(' + ')"
>
+
</button>
<button
class="bg-base-200 px-2 rounded-xl"
@click="insertTextUnderCursor(' - ')"
>
-
</button>
<button
class="bg-base-200 px-2 rounded-xl"
@click="insertTextUnderCursor(' * ')"
>
*
</button>
<button
class="bg-base-200 px-2 rounded-xl"
@click="insertTextUnderCursor(' / ')"
>
/
</button>
<button
class="bg-base-200 px-2 rounded-xl"
@click="insertTextUnderCursor(' % ')"
>
%
</button>
<button
class="bg-base-200 px-2 rounded-xl"
@click="insertTextUnderCursor('^')"
>
^
</button>
<button
class="bg-base-200 px-2 rounded-xl"
@click="insertTextUnderCursor('round()')"
>
round(n, d)
</button>
<button
class="bg-base-200 px-2 rounded-xl"
@click="insertTextUnderCursor('abs()')"
>
abs(n)
</button>
</div>
</div>
</div>
<button
class="base-button w-full"
@click.prevent="validateSaveAndClose"
>
{{ t('save') }}
</button>
</div>
</div>
</div>
</template>
<script>
import { IconCodePlus } from '@tabler/icons-vue'
export default {
name: 'FormulaModal',
components: {
IconCodePlus
},
inject: ['t', 'save', 'template'],
props: {
field: {
type: Object,
required: true
},
buildDefaultName: {
type: Function,
required: true
}
},
emits: ['close'],
data () {
return {
formula: ''
}
},
computed: {
fields () {
return this.template.fields.reduce((acc, f) => {
if (f !== this.field && f.submitter_uuid === this.field.submitter_uuid && ['number'].includes(f.type) && !f.preferences?.formula) {
acc.push(f)
}
return acc
}, [])
}
},
created () {
this.field.preferences ||= {}
},
mounted () {
this.formula = this.humanizeFormula(this.field.preferences.formula || '')
},
methods: {
humanizeFormula (text) {
return text.replace(/{{(.*?)}}/g, (match, uuid) => {
const foundField = this.fields.find((f) => f.uuid === uuid)
if (foundField) {
return `{{${foundField.name || this.buildDefaultName(foundField, this.template.fields)}}}`
} else {
return '{{FIELD NOT FOUND}}'
}
})
},
normalizeFormula (text) {
return text.replace(/{{(.*?)}}/g, (match, name) => {
const foundField = this.fields.find((f) => {
return (f.name || this.buildDefaultName(f, this.template.fields)).trim() === name.trim()
})
if (foundField) {
return `{{${foundField.uuid}}}`
} else {
return '{{FIELD NOT FOUND}}'
}
})
},
validateSaveAndClose () {
const normalizedFormula = this.normalizeFormula(this.formula)
if (normalizedFormula.includes('FIELD NOT FOUND')) {
alert('Some fields are missing in the formula.')
} else {
this.field.preferences.formula = normalizedFormula
this.field.readonly = !!normalizedFormula
this.save()
this.$emit('close')
}
},
insertTextUnderCursor (textToInsert) {
const textarea = this.$refs.textarea
const selectionEnd = textarea.selectionEnd
const cursorPos = selectionEnd
const newText = textarea.value.substring(0, cursorPos) + textToInsert + textarea.value.substring(cursorPos)
this.formula = newText
textarea.setSelectionRange(cursorPos + textToInsert.length, cursorPos + textToInsert.length)
textarea.focus()
},
resizeTextarea () {
const textarea = this.$refs.textarea
textarea.style.height = 'auto'
textarea.style.height = textarea.scrollHeight + 'px'
}
}
}
</script>
+1
View File
@@ -13,6 +13,7 @@ const en = {
cancel: 'Cancel',
any: 'Any',
drawn: 'Drawn',
formula: 'Formula',
typed: 'Typed',
draw_field_on_the_document: 'Draw {field} field on the document',
click_to_upload: 'Click to upload',