Merge from docusealco/wip
@@ -28,7 +28,7 @@ Lint/MissingSuper:
|
|||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Metrics/ParameterLists:
|
Metrics/ParameterLists:
|
||||||
Max: 10
|
Max: 12
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 30
|
Max: 30
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache libpq vips redis vips-heif fontconfig onnxruntime
|
RUN apk add --no-cache libpq vips redis vips-heif onnxruntime
|
||||||
|
|
||||||
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
render json: { field_uuid: e.message }, status: :unprocessable_content
|
render json: { field_uuid: e.message }, status: :unprocessable_content
|
||||||
rescue Submitters::SubmitValues::ValidationError => e
|
rescue Submitters::SubmitValues::ValidationError => e
|
||||||
|
Rollbar.warning("Validation error #{@submitter.id}: #{e.message}") if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_content
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormMetadataController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def index
|
||||||
|
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
|
return head :not_found if submitter.declined_at? ||
|
||||||
|
submitter.completed_at? ||
|
||||||
|
submitter.submission.archived_at? ||
|
||||||
|
submitter.submission.expired? ||
|
||||||
|
submitter.submission.template&.archived_at? ||
|
||||||
|
submitter.account.archived_at? ||
|
||||||
|
!Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||||
|
|
||||||
|
submission = submitter.submission
|
||||||
|
values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
|
||||||
|
schema = Submissions.filtered_conditions_schema(submission, values:, include_submitter_uuid: submitter.uuid)
|
||||||
|
|
||||||
|
documents = schema.filter_map do |item|
|
||||||
|
submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(records: documents, associations: %i[blob record]).call
|
||||||
|
|
||||||
|
text_runs = documents.to_h do |document|
|
||||||
|
[
|
||||||
|
document.uuid,
|
||||||
|
DocumentMetadatas.find_or_create_for_document(document, account_id: document.record.account_id).text_runs
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: { text_runs: }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesShareLinkQrController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def show
|
||||||
|
return render :disabled, layout: 'plain' unless @template.shared_link?
|
||||||
|
|
||||||
|
shared_link_url = start_form_url(slug: @template.slug, host: form_link_host)
|
||||||
|
|
||||||
|
@qr_svg_code = RQRCode::QRCode.new(shared_link_url, level: :m).as_svg(viewbox: true)
|
||||||
|
|
||||||
|
@page_size =
|
||||||
|
if TimeUtils.timezone_abbr(current_account.timezone, Time.current.beginning_of_year).in?(TimeUtils::US_TIMEZONES)
|
||||||
|
'Letter'
|
||||||
|
else
|
||||||
|
'A4'
|
||||||
|
end
|
||||||
|
|
||||||
|
render :show, layout: false
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -32,9 +32,13 @@ class WebhookSettingsController < ApplicationController
|
|||||||
def new; end
|
def new; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@webhook_url.save!
|
if @webhook_url.url.present?
|
||||||
|
@webhook_url.save!
|
||||||
|
|
||||||
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
|
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
|
||||||
|
else
|
||||||
|
redirect_back fallback_location: settings_webhooks_path
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ export default targetable(class extends HTMLElement {
|
|||||||
|
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.addEventListener('click', () => this.downloadFiles())
|
this.addEventListener('click', () => this.downloadFiles())
|
||||||
|
this.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
this.downloadFiles()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleState () {
|
toggleState () {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const dialog = document.getElementById(this.dataset.target)
|
||||||
|
|
||||||
|
this.querySelector('button').addEventListener('click', () => {
|
||||||
|
if (dialog) {
|
||||||
|
dialog.inert = false
|
||||||
|
dialog.showModal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (dialog) {
|
||||||
|
dialog.addEventListener('close', () => {
|
||||||
|
dialog.inert = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,11 +47,13 @@ export default class extends HTMLElement {
|
|||||||
|
|
||||||
this.classList.remove('hidden', '-translate-y-10', 'opacity-0')
|
this.classList.remove('hidden', '-translate-y-10', 'opacity-0')
|
||||||
this.classList.add('translate-y-0', 'opacity-100')
|
this.classList.add('translate-y-0', 'opacity-100')
|
||||||
|
this.inert = false
|
||||||
}
|
}
|
||||||
|
|
||||||
hideButtons () {
|
hideButtons () {
|
||||||
this.classList.remove('translate-y-0', 'opacity-100')
|
this.classList.remove('translate-y-0', 'opacity-100')
|
||||||
this.classList.add('-translate-y-10', 'opacity-0')
|
this.classList.add('-translate-y-10', 'opacity-0')
|
||||||
|
this.inert = true
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.classList.contains('-translate-y-10')) {
|
if (this.classList.contains('-translate-y-10')) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import FetchForm from './elements/fetch_form'
|
|||||||
import ScrollButtons from './elements/scroll_buttons'
|
import ScrollButtons from './elements/scroll_buttons'
|
||||||
import PageContainer from './elements/page_container'
|
import PageContainer from './elements/page_container'
|
||||||
import SubmitForm from './elements/submit_form'
|
import SubmitForm from './elements/submit_form'
|
||||||
|
import ModalButton from './elements/modal_button'
|
||||||
|
|
||||||
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ safeRegisterElement('fetch-form', FetchForm)
|
|||||||
safeRegisterElement('scroll-buttons', ScrollButtons)
|
safeRegisterElement('scroll-buttons', ScrollButtons)
|
||||||
safeRegisterElement('page-container', PageContainer)
|
safeRegisterElement('page-container', PageContainer)
|
||||||
safeRegisterElement('submit-form', SubmitForm)
|
safeRegisterElement('submit-form', SubmitForm)
|
||||||
|
safeRegisterElement('modal-button', ModalButton)
|
||||||
safeRegisterElement('submission-form', class extends HTMLElement {
|
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|||||||
@@ -0,0 +1,434 @@
|
|||||||
|
<template>
|
||||||
|
<template
|
||||||
|
v-for="(pages, docUuid) in textRuns"
|
||||||
|
:key="docUuid"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(_, pageIndex) in pages"
|
||||||
|
:key="pageIndex"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(pageElem, i) in [findPageElement(docUuid, pageIndex)]"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
<Teleport
|
||||||
|
v-if="pageElem"
|
||||||
|
:to="pageElem"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(item, index) in sortedItemsForPage(docUuid, pageIndex)"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<template v-if="item.type === 'text_group'">
|
||||||
|
<span
|
||||||
|
class="absolute overflow-hidden text-transparent select-none pointer-events-none"
|
||||||
|
:style="{ left: item.x * 100 + '%', top: item.y * 100 + '%', width: item.w * 100 + '%', height: item.h * 100 + '%' }"
|
||||||
|
>{{ item.text }}</span>
|
||||||
|
<span
|
||||||
|
v-for="(run, runIndex) in item.items"
|
||||||
|
:key="runIndex"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="absolute overflow-hidden text-transparent"
|
||||||
|
:style="{ left: run.x * 100 + '%', top: run.y * 100 + '%', width: run.w * 100 + '%', height: run.h * 100 + '%', fontSize: run.font_size ? (run.font_size / 10) + 'cqmin' : undefined, textAlign: 'justify', textAlignLast: 'justify', textJustify: 'inter-character' }"
|
||||||
|
>{{ run.text }}</span>
|
||||||
|
</template>
|
||||||
|
<FieldArea
|
||||||
|
v-else-if="item.type === 'field_area'"
|
||||||
|
:ref="setAreaRef"
|
||||||
|
v-model="values[item.field.uuid]"
|
||||||
|
:values="values"
|
||||||
|
:field="item.field"
|
||||||
|
:area="item.area"
|
||||||
|
:submittable="true"
|
||||||
|
:page-width="1400"
|
||||||
|
:page-height="(1400.0 / pageElem.offsetWidth) * pageElem.offsetHeight"
|
||||||
|
:field-index="item.fieldIndex"
|
||||||
|
:is-inline-size="isInlineSize"
|
||||||
|
:scroll-padding="scrollPadding"
|
||||||
|
:submitter="submitter"
|
||||||
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
|
:is-active="currentStep === item.step"
|
||||||
|
:with-label="withLabel && !withFieldPlaceholder && item.step.length < 2"
|
||||||
|
:is-value-set="item.step.some((f) => f.uuid in values)"
|
||||||
|
:attachments-index="attachmentsIndex"
|
||||||
|
@click="[$emit('focus-step', item.stepIndex), maybeScrollOnClick(item.field, item.area)]"
|
||||||
|
/>
|
||||||
|
<FieldArea
|
||||||
|
v-else-if="item.type === 'readonly_field_area'"
|
||||||
|
:model-value="readonlyConditionalFieldValues[item.field.uuid]"
|
||||||
|
:values="readonlyConditionalFieldValues"
|
||||||
|
:field="item.field"
|
||||||
|
:area="item.area"
|
||||||
|
:submittable="false"
|
||||||
|
:page-width="1400"
|
||||||
|
:page-height="(1400.0 / pageElem.offsetWidth) * pageElem.offsetHeight"
|
||||||
|
:field-index="item.fieldIndex"
|
||||||
|
:is-inline-size="isInlineSize"
|
||||||
|
:submitter="submitter"
|
||||||
|
:attachments-index="attachmentsIndex"
|
||||||
|
/>
|
||||||
|
<FieldArea
|
||||||
|
v-else-if="item.type === 'formula_area' && isMathLoaded"
|
||||||
|
:model-value="calculateFormula(item.field)"
|
||||||
|
:is-inline-size="isInlineSize"
|
||||||
|
:field="item.field"
|
||||||
|
:area="item.area"
|
||||||
|
:submittable="false"
|
||||||
|
:field-index="item.fieldIndex"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FieldArea from './area'
|
||||||
|
import FormulaAreas from './formula_areas'
|
||||||
|
import FieldAreas from './areas'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AccessibilityAreas',
|
||||||
|
components: {
|
||||||
|
FieldArea
|
||||||
|
},
|
||||||
|
inject: ['baseUrl', 't'],
|
||||||
|
props: {
|
||||||
|
submitterSlug: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
filledFieldsIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
steps: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
readonlyConditionalFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
readonlyConditionalFieldValues: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
formulaFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
readonlyValues: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
submitter: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
currentStep: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
withFieldPlaceholder: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withLabel: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
scrollPadding: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '-80px'
|
||||||
|
},
|
||||||
|
scrollEl: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
attachmentsIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
fetchOptions: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['focus-step'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isMathLoaded: false,
|
||||||
|
math: null,
|
||||||
|
textRuns: {},
|
||||||
|
areaRefs: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fieldValuesIndex () {
|
||||||
|
return this.filledFieldsIndex || this.extractStaticValues()
|
||||||
|
},
|
||||||
|
isMobileContainer: FieldAreas.computed.isMobileContainer,
|
||||||
|
isInlineSize: FieldAreas.computed.isInlineSize,
|
||||||
|
fieldsUuidIndex () {
|
||||||
|
return this.formulaFields.reduce((acc, field) => {
|
||||||
|
acc[field.uuid] = field
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
fieldAreasIndex () {
|
||||||
|
const index = Object.create(null)
|
||||||
|
|
||||||
|
this.steps.forEach((step, stepIndex) => {
|
||||||
|
step.forEach((field, fieldIndex) => {
|
||||||
|
(field.areas || []).forEach((area) => {
|
||||||
|
index[area.attachment_uuid] ||= Object.create(null)
|
||||||
|
index[area.attachment_uuid][area.page] ||= []
|
||||||
|
index[area.attachment_uuid][area.page].push({
|
||||||
|
type: 'field_area',
|
||||||
|
field,
|
||||||
|
area,
|
||||||
|
step,
|
||||||
|
stepIndex,
|
||||||
|
fieldIndex,
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
w: area.w,
|
||||||
|
h: area.h
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return index
|
||||||
|
},
|
||||||
|
formulaAreasIndex () {
|
||||||
|
const index = Object.create(null)
|
||||||
|
|
||||||
|
this.formulaFields.forEach((field, fieldIndex) => {
|
||||||
|
(field.areas || []).forEach((area) => {
|
||||||
|
index[area.attachment_uuid] ||= Object.create(null)
|
||||||
|
index[area.attachment_uuid][area.page] ||= []
|
||||||
|
index[area.attachment_uuid][area.page].push({
|
||||||
|
type: 'formula_area',
|
||||||
|
field,
|
||||||
|
area,
|
||||||
|
fieldIndex,
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
w: area.w,
|
||||||
|
h: area.h
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return index
|
||||||
|
},
|
||||||
|
readonlyFieldAreasIndex () {
|
||||||
|
const index = Object.create(null)
|
||||||
|
|
||||||
|
this.readonlyConditionalFields.forEach((field, fieldIndex) => {
|
||||||
|
(field.areas || []).forEach((area) => {
|
||||||
|
index[area.attachment_uuid] ||= Object.create(null)
|
||||||
|
index[area.attachment_uuid][area.page] ||= []
|
||||||
|
index[area.attachment_uuid][area.page].push({
|
||||||
|
type: 'readonly_field_area',
|
||||||
|
field,
|
||||||
|
area,
|
||||||
|
fieldIndex,
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
w: area.w,
|
||||||
|
h: area.h
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUpdate () {
|
||||||
|
this.areaRefs = []
|
||||||
|
},
|
||||||
|
async mounted () {
|
||||||
|
const [metadataResult] = await Promise.all([
|
||||||
|
fetch(this.baseUrl + `/s/${this.submitterSlug}/metadata`, {
|
||||||
|
...this.fetchOptions
|
||||||
|
}).then((r) => r.json()).catch(() => ({})),
|
||||||
|
this.loadMath()
|
||||||
|
])
|
||||||
|
|
||||||
|
this.textRuns = metadataResult.text_runs || {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
normalizeFormula: FormulaAreas.methods.normalizeFormula,
|
||||||
|
calculateFormula: FormulaAreas.methods.calculateFormula,
|
||||||
|
scrollInContainer: FieldAreas.methods.scrollInContainer,
|
||||||
|
scrollIntoArea: FieldAreas.methods.scrollIntoArea,
|
||||||
|
scrollIntoField: FieldAreas.methods.scrollIntoField,
|
||||||
|
maybeScrollOnClick: FieldAreas.methods.maybeScrollOnClick,
|
||||||
|
setAreaRef (el) {
|
||||||
|
if (el) {
|
||||||
|
this.areaRefs.push(el)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadMath () {
|
||||||
|
if (this.formulaFields.length && !this.isMathLoaded) {
|
||||||
|
const { Calculator } = await import('./calculator')
|
||||||
|
|
||||||
|
this.math = new Calculator()
|
||||||
|
this.isMathLoaded = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extractStaticValues () {
|
||||||
|
const result = Object.create(null)
|
||||||
|
const root = this.$root.$el?.parentNode?.getRootNode() || document
|
||||||
|
const pageContainers = root.querySelectorAll('page-container')
|
||||||
|
|
||||||
|
pageContainers.forEach((container) => {
|
||||||
|
const overlay = container.querySelector('[id^="page-"]')
|
||||||
|
|
||||||
|
if (!overlay) return
|
||||||
|
|
||||||
|
const parts = overlay.id.split('-')
|
||||||
|
const pageIndex = parseInt(parts[parts.length - 1])
|
||||||
|
const docUuid = parts.slice(1, -1).join('-')
|
||||||
|
|
||||||
|
const fieldValues = overlay.querySelectorAll('field-value')
|
||||||
|
|
||||||
|
if (!fieldValues.length) return
|
||||||
|
|
||||||
|
result[docUuid] ||= Object.create(null)
|
||||||
|
result[docUuid][pageIndex] = []
|
||||||
|
|
||||||
|
fieldValues.forEach((el) => {
|
||||||
|
const style = el.style
|
||||||
|
const x = parseFloat(style.left) / 100
|
||||||
|
const y = parseFloat(style.top) / 100
|
||||||
|
const w = parseFloat(style.width) / 100
|
||||||
|
const h = parseFloat(style.height) / 100
|
||||||
|
const text = el.textContent.trim()
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
result[docUuid][pageIndex].push({ type: 'static_value', text, x, y, w, h })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
findPageElement (docUuid, pageIndex) {
|
||||||
|
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${docUuid}-${pageIndex}`)
|
||||||
|
},
|
||||||
|
sortedItemsForPage (docUuid, pageIndex) {
|
||||||
|
const items = []
|
||||||
|
|
||||||
|
const pageTextRuns = this.textRuns[docUuid]?.[pageIndex] || []
|
||||||
|
|
||||||
|
pageTextRuns.forEach((run) => {
|
||||||
|
items.push({
|
||||||
|
type: 'text_run',
|
||||||
|
text: run.text,
|
||||||
|
x: run.x,
|
||||||
|
y: run.y,
|
||||||
|
w: run.w,
|
||||||
|
h: run.h,
|
||||||
|
font_size: run.font_size
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const fieldAreas = this.fieldAreasIndex[docUuid]?.[pageIndex] || []
|
||||||
|
items.push(...fieldAreas)
|
||||||
|
|
||||||
|
const readonlyFieldAreas = this.readonlyFieldAreasIndex[docUuid]?.[pageIndex] || []
|
||||||
|
items.push(...readonlyFieldAreas)
|
||||||
|
|
||||||
|
const formulaAreas = this.formulaAreasIndex[docUuid]?.[pageIndex] || []
|
||||||
|
items.push(...formulaAreas)
|
||||||
|
|
||||||
|
const pageFieldValues = this.fieldValuesIndex[docUuid]?.[pageIndex] || []
|
||||||
|
items.push(...pageFieldValues)
|
||||||
|
|
||||||
|
items.sort((a, b) => {
|
||||||
|
const aCenterY = a.y + a.h / 2
|
||||||
|
const bCenterY = b.y + b.h / 2
|
||||||
|
const lineThreshold = Math.min(a.h, b.h) / 2
|
||||||
|
|
||||||
|
if (Math.abs(aCenterY - bCenterY) < lineThreshold) {
|
||||||
|
return a.x - b.x
|
||||||
|
}
|
||||||
|
|
||||||
|
return aCenterY - bCenterY
|
||||||
|
})
|
||||||
|
|
||||||
|
const grouped = []
|
||||||
|
let currentGroup = null
|
||||||
|
|
||||||
|
const closeGroup = () => {
|
||||||
|
if (!currentGroup) return
|
||||||
|
|
||||||
|
const groupItems = currentGroup.items
|
||||||
|
const minX = Math.min(...groupItems.map((i) => i.x))
|
||||||
|
const minY = Math.min(...groupItems.map((i) => i.y))
|
||||||
|
const maxEndX = Math.max(...groupItems.map((i) => i.x + i.w))
|
||||||
|
const maxEndY = Math.max(...groupItems.map((i) => i.y + i.h))
|
||||||
|
|
||||||
|
currentGroup.x = minX
|
||||||
|
currentGroup.y = minY
|
||||||
|
currentGroup.w = maxEndX - minX
|
||||||
|
currentGroup.h = maxEndY - minY
|
||||||
|
currentGroup.text = groupItems.map((i) => i.text).join(' ').replace(/\s+/g, ' ').trim()
|
||||||
|
|
||||||
|
grouped.push(currentGroup)
|
||||||
|
currentGroup = null
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const isTextLike = item.type === 'text_run' || item.type === 'static_value'
|
||||||
|
|
||||||
|
if (!isTextLike) {
|
||||||
|
closeGroup()
|
||||||
|
grouped.push(item)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentGroup) {
|
||||||
|
currentGroup = { type: 'text_group', items: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
currentGroup.items.push(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
closeGroup()
|
||||||
|
|
||||||
|
return grouped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex absolute lg:text-base -outline-offset-1 field-area"
|
class="flex absolute lg:text-base -outline-offset-1 focus-visible:outline-blue-500 focus-visible:outline-2 focus-visible:outline field-area"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:style="[computedStyle, fontStyle]"
|
:style="[computedStyle, fontStyle]"
|
||||||
:class="{ 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
:class="{ 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
||||||
|
:role="submittable && !isNativeInputField ? 'button' : undefined"
|
||||||
|
:tabindex="submittable && !isNativeInputField ? 0 : undefined"
|
||||||
|
:aria-label="submittable && !isNativeInputField ? fieldAreaLabel : undefined"
|
||||||
|
@keydown.enter.prevent="submittable && !isNativeInputField ? $el.click() : undefined"
|
||||||
|
@keydown.space.prevent="submittable && !isNativeInputField ? $el.click() : undefined"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
||||||
@@ -18,6 +23,7 @@
|
|||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
class="max-h-10 text-base-content"
|
class="max-h-10 text-base-content"
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,6 +135,7 @@
|
|||||||
v-if="submittable"
|
v-if="submittable"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:value="false"
|
:value="false"
|
||||||
|
:aria-label="field.name || fieldNames[field.type]"
|
||||||
class="aspect-square base-checkbox"
|
class="aspect-square base-checkbox"
|
||||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
:checked="!!modelValue"
|
:checked="!!modelValue"
|
||||||
@@ -149,6 +156,8 @@
|
|||||||
v-if="submittable"
|
v-if="submittable"
|
||||||
type="radio"
|
type="radio"
|
||||||
:value="false"
|
:value="false"
|
||||||
|
:name="`radio-area-${field.uuid}`"
|
||||||
|
:aria-label="optionValue(option)"
|
||||||
class="aspect-square checked:checkbox checked:checkbox-xs"
|
class="aspect-square checked:checkbox checked:checkbox-xs"
|
||||||
:class="{ 'base-radio': !modelValue || modelValue !== optionValue(option), '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
:class="{ 'base-radio': !modelValue || modelValue !== optionValue(option), '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
:checked="!!modelValue && modelValue === optionValue(option)"
|
:checked="!!modelValue && modelValue === optionValue(option)"
|
||||||
@@ -169,6 +178,7 @@
|
|||||||
v-if="submittable"
|
v-if="submittable"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:value="false"
|
:value="false"
|
||||||
|
:aria-label="optionValue(option)"
|
||||||
class="aspect-square base-checkbox"
|
class="aspect-square base-checkbox"
|
||||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
:checked="!!modelValue && modelValue.includes(optionValue(option))"
|
:checked="!!modelValue && modelValue.includes(optionValue(option))"
|
||||||
@@ -400,6 +410,16 @@ export default {
|
|||||||
kba: this.t('kba')
|
kba: this.t('kba')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isNativeInputField () {
|
||||||
|
return ['checkbox', 'radio', 'multiple'].includes(this.field.type)
|
||||||
|
},
|
||||||
|
fieldAreaLabel () {
|
||||||
|
const name = this.field.name || this.fieldNames[this.field.type] || this.field.type
|
||||||
|
if (this.area.option_uuid && this.option) {
|
||||||
|
return `${name} - ${this.optionValue(this.option)}`
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
},
|
||||||
strikethroughWidth () {
|
strikethroughWidth () {
|
||||||
if (this.isInlineSize) {
|
if (this.isInlineSize) {
|
||||||
return '0.6cqmin'
|
return '0.6cqmin'
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="value.length">
|
<ul
|
||||||
<div
|
v-if="value.length"
|
||||||
|
:aria-label="t('uploaded_files')"
|
||||||
|
class="list-none p-0 m-0"
|
||||||
|
>
|
||||||
|
<li
|
||||||
v-for="(val, index) in value"
|
v-for="(val, index) in value"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="flex mb-2"
|
class="flex mb-2"
|
||||||
@@ -20,23 +24,27 @@
|
|||||||
<IconPaperclip
|
<IconPaperclip
|
||||||
:width="16"
|
:width="16"
|
||||||
class="flex-none"
|
class="flex-none"
|
||||||
:heigh="16"
|
:height="16"
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{{ attachmentsIndex[val].filename }}
|
{{ attachmentsIndex[val].filename }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="remove-attachment-button"
|
class="remove-attachment-button"
|
||||||
|
:aria-label="`${t('clear')} ${attachmentsIndex[val].filename}`"
|
||||||
@click.prevent="removeAttachment(val)"
|
@click.prevent="removeAttachment(val)"
|
||||||
>
|
>
|
||||||
<IconTrashX
|
<IconTrashX
|
||||||
:width="18"
|
:width="18"
|
||||||
:heigh="19"
|
:height="19"
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<input
|
<input
|
||||||
value=""
|
value=""
|
||||||
|
|||||||
@@ -3,10 +3,13 @@
|
|||||||
id="form_completed"
|
id="form_completed"
|
||||||
class="mx-auto max-w-md flex flex-col completed-form"
|
class="mx-auto max-w-md flex flex-col completed-form"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
role="status"
|
||||||
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
|
<div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
|
||||||
<IconCircleCheck
|
<IconCircleCheck
|
||||||
class="inline text-green-600"
|
class="inline text-green-600"
|
||||||
|
aria-hidden="true"
|
||||||
:width="30"
|
:width="30"
|
||||||
:height="30"
|
:height="30"
|
||||||
/>
|
/>
|
||||||
@@ -42,8 +45,12 @@
|
|||||||
<IconInnerShadowTop
|
<IconInnerShadowTop
|
||||||
v-if="isSendingCopy"
|
v-if="isSendingCopy"
|
||||||
class="animate-spin"
|
class="animate-spin"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<IconMail
|
||||||
|
v-else
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<IconMail v-else />
|
|
||||||
<span>
|
<span>
|
||||||
{{ t('send_copy_via_email') }}
|
{{ t('send_copy_via_email') }}
|
||||||
</span>
|
</span>
|
||||||
@@ -57,8 +64,12 @@
|
|||||||
<IconInnerShadowTop
|
<IconInnerShadowTop
|
||||||
v-if="isDownloading"
|
v-if="isDownloading"
|
||||||
class="animate-spin"
|
class="animate-spin"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<IconDownload
|
||||||
|
v-else
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<IconDownload v-else />
|
|
||||||
<span>
|
<span>
|
||||||
{{ t('download') }}
|
{{ t('download') }}
|
||||||
</span>
|
</span>
|
||||||
@@ -199,7 +210,7 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll('#decline_button').forEach((button) => {
|
document.querySelectorAll('#decline_button, #decline_button_mobile, #delegate_button, #delegate_button_mobile').forEach((button) => {
|
||||||
button.setAttribute('disabled', 'true')
|
button.setAttribute('disabled', 'true')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,12 +30,16 @@
|
|||||||
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
|
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
|
||||||
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
||||||
>
|
>
|
||||||
<IconCalendarCheck :width="16" />
|
<IconCalendarCheck
|
||||||
|
:width="16"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
{{ t('set_today') }}
|
{{ t('set_today') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
|
:id="field.uuid + '-desc'"
|
||||||
class="mb-3 px-1 field-description-text"
|
class="mb-3 px-1 field-description-text"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
@@ -51,6 +55,7 @@
|
|||||||
:max="validationMax"
|
:max="validationMax"
|
||||||
class="base-input !text-2xl text-center w-full"
|
class="base-input !text-2xl text-center w-full"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
|
:aria-describedby="field.description ? field.uuid + '-desc' : undefined"
|
||||||
type="date"
|
type="date"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
@keydown.enter="onEnter"
|
@keydown.enter="onEnter"
|
||||||
|
|||||||
@@ -39,8 +39,9 @@
|
|||||||
ref="input"
|
ref="input"
|
||||||
:multiple="multiple"
|
:multiple="multiple"
|
||||||
:accept="accept"
|
:accept="accept"
|
||||||
|
:aria-label="message"
|
||||||
type="file"
|
type="file"
|
||||||
class="hidden"
|
class="sr-only"
|
||||||
@change="onSelectFiles"
|
@change="onSelectFiles"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -1,5 +1,52 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<Teleport
|
||||||
|
v-if="withAccessibilityAreas === null && !isAccessibilityMode"
|
||||||
|
to="#sr_only_content"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-[100] focus:px-4 focus:py-2 focus:bg-base-100 focus:text-base-content focus:rounded focus:shadow-lg"
|
||||||
|
@click="isAccessibilityMode = true"
|
||||||
|
>
|
||||||
|
{{ t('enter_screen_reader_mode') }}
|
||||||
|
</button>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-for="item in (withAccessibilityAreas === null ? schema : [])"
|
||||||
|
:key="item.attachment_uuid"
|
||||||
|
:to="`#document-${item.attachment_uuid} .sr_only_content`"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="!isAccessibilityMode"
|
||||||
|
class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-[100] focus:px-4 focus:py-2 focus:bg-base-100 focus:text-base-content focus:rounded focus:shadow-lg"
|
||||||
|
@click="isAccessibilityMode = true"
|
||||||
|
>
|
||||||
|
{{ t('enter_screen_reader_mode') }}
|
||||||
|
</button>
|
||||||
|
</Teleport>
|
||||||
|
<AccessibilityAreas
|
||||||
|
v-if="withAccessibilityAreas || isAccessibilityMode"
|
||||||
|
ref="areas"
|
||||||
|
:submitter-slug="submitterSlug"
|
||||||
|
:steps="stepFields"
|
||||||
|
:readonly-conditional-fields="readonlyConditionalFields"
|
||||||
|
:readonly-conditional-field-values="readonlyConditionalFieldValues"
|
||||||
|
:formula-fields="formulaFields"
|
||||||
|
:values="values"
|
||||||
|
:readonly-values="readonlyFieldValues"
|
||||||
|
:submitter="submitter"
|
||||||
|
:scroll-el="scrollEl"
|
||||||
|
:current-step="currentStepFields"
|
||||||
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
|
:with-label="withFieldLabels && !isAnonymousChecboxes && showFieldNames"
|
||||||
|
:scroll-padding="scrollPadding"
|
||||||
|
:attachments-index="attachmentsIndex"
|
||||||
|
:fetch-options="fetchOptions"
|
||||||
|
:filled-fields-index="filledFieldsIndex"
|
||||||
|
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
||||||
|
/>
|
||||||
<FieldAreas
|
<FieldAreas
|
||||||
|
v-if="!withAccessibilityAreas && !isAccessibilityMode"
|
||||||
ref="areas"
|
ref="areas"
|
||||||
:steps="stepFields"
|
:steps="stepFields"
|
||||||
:values="values"
|
:values="values"
|
||||||
@@ -14,6 +61,7 @@
|
|||||||
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
||||||
/>
|
/>
|
||||||
<FieldAreas
|
<FieldAreas
|
||||||
|
v-if="!withAccessibilityAreas && !isAccessibilityMode"
|
||||||
:steps="readonlyConditionalFields.map((e) => [e])"
|
:steps="readonlyConditionalFields.map((e) => [e])"
|
||||||
:values="readonlyConditionalFieldValues"
|
:values="readonlyConditionalFieldValues"
|
||||||
:submitter="submitter"
|
:submitter="submitter"
|
||||||
@@ -21,7 +69,7 @@
|
|||||||
:submittable="false"
|
:submittable="false"
|
||||||
/>
|
/>
|
||||||
<FormulaFieldAreas
|
<FormulaFieldAreas
|
||||||
v-if="formulaFields.length"
|
v-if="!withAccessibilityAreas && !isAccessibilityMode && formulaFields.length"
|
||||||
:fields="formulaFields"
|
:fields="formulaFields"
|
||||||
:readonly-values="readonlyFieldValues"
|
:readonly-values="readonlyFieldValues"
|
||||||
:values="values"
|
:values="values"
|
||||||
@@ -57,6 +105,7 @@
|
|||||||
<IconInnerShadowTop
|
<IconInnerShadowTop
|
||||||
v-if="isSubmittingComplete"
|
v-if="isSubmittingComplete"
|
||||||
class="mr-1 animate-spin w-5 h-5"
|
class="mr-1 animate-spin w-5 h-5"
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{{ t('complete') }}
|
{{ t('complete') }}
|
||||||
@@ -88,6 +137,7 @@
|
|||||||
class="absolute right-0 mr-4"
|
class="absolute right-0 mr-4"
|
||||||
:width="20"
|
:width="20"
|
||||||
:height="20"
|
:height="20"
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
@@ -103,11 +153,13 @@
|
|||||||
class="absolute right-0 top-0 minimize-form-button"
|
class="absolute right-0 top-0 minimize-form-button"
|
||||||
:class="currentField?.description?.length > 100 ? 'mr-1 mt-1 md:mr-2 md:mt-2': 'mr-2 mt-2 hidden md:block'"
|
:class="currentField?.description?.length > 100 ? 'mr-1 mt-1 md:mr-2 md:mt-2': 'mr-2 mt-2 hidden md:block'"
|
||||||
:title="t('minimize')"
|
:title="t('minimize')"
|
||||||
|
:aria-label="t('minimize')"
|
||||||
@click.prevent="minimizeForm"
|
@click.prevent="minimizeForm"
|
||||||
>
|
>
|
||||||
<IconArrowsDiagonalMinimize2
|
<IconArrowsDiagonalMinimize2
|
||||||
:width="20"
|
:width="20"
|
||||||
:height="20"
|
:height="20"
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
@@ -193,6 +245,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="currentField.description"
|
v-if="currentField.description"
|
||||||
|
:id="currentField.uuid + '-desc'"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1 field-description-text"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
@@ -203,6 +256,7 @@
|
|||||||
:id="currentField.uuid"
|
:id="currentField.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:required="currentField.required"
|
:required="currentField.required"
|
||||||
|
:aria-describedby="currentField.description ? currentField.uuid + '-desc' : undefined"
|
||||||
class="select base-input !text-2xl w-full text-center font-normal"
|
class="select base-input !text-2xl w-full text-center font-normal"
|
||||||
:class="{ 'text-gray-300': !values[currentField.uuid] }"
|
:class="{ 'text-gray-300': !values[currentField.uuid] }"
|
||||||
:name="`values[${currentField.uuid}]`"
|
:name="`values[${currentField.uuid}]`"
|
||||||
@@ -250,6 +304,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-if="currentField.description"
|
v-if="currentField.description"
|
||||||
|
:id="currentField.uuid + '-desc'"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1 field-description-text"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
@@ -309,6 +364,7 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="currentField.description"
|
v-if="currentField.description"
|
||||||
|
:id="currentField.uuid + '-desc'"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1 field-description-text"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
@@ -511,6 +567,7 @@
|
|||||||
<IconInnerShadowTop
|
<IconInnerShadowTop
|
||||||
v-if="isSubmitting"
|
v-if="isSubmitting"
|
||||||
class="mr-1 animate-spin"
|
class="mr-1 animate-spin"
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{{ submitButtonText }}
|
{{ submitButtonText }}
|
||||||
@@ -522,6 +579,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-if="showFillAllRequiredFields"
|
v-if="showFillAllRequiredFields"
|
||||||
|
role="alert"
|
||||||
class="text-center mt-1"
|
class="text-center mt-1"
|
||||||
>
|
>
|
||||||
{{ t('please_fill_all_required_fields') }}
|
{{ t('please_fill_all_required_fields') }}
|
||||||
@@ -554,8 +612,10 @@
|
|||||||
:can-send-email="canSendEmail && !!submitter.email"
|
:can-send-email="canSendEmail && !!submitter.email"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
/>
|
/>
|
||||||
<div
|
<nav
|
||||||
v-if="stepFields.length < 80"
|
v-if="stepFields.length < 80"
|
||||||
|
:aria-label="t('form_progress')"
|
||||||
|
:aria-hidden="isCompleted"
|
||||||
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1 select-none"
|
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1 select-none"
|
||||||
>
|
>
|
||||||
<div class="flex items-center flex-wrap steps-progress">
|
<div class="flex items-center flex-wrap steps-progress">
|
||||||
@@ -563,16 +623,18 @@
|
|||||||
v-for="(step, index) in stepFields"
|
v-for="(step, index) in stepFields"
|
||||||
:key="step[0].uuid"
|
:key="step[0].uuid"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
v-if="!onlyRequiredFields || step.some((f) => f.required)"
|
v-if="!onlyRequiredFields || step.some((f) => f.required)"
|
||||||
href="#"
|
type="button"
|
||||||
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
|
:aria-label="`${t('step')} ${index + 1}`"
|
||||||
|
:aria-current="index === currentStep ? 'step' : undefined"
|
||||||
|
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1 p-0"
|
||||||
:class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
:class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
||||||
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
|
@click="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="mt-5"
|
class="mt-5"
|
||||||
@@ -584,6 +646,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import FieldAreas from './areas'
|
import FieldAreas from './areas'
|
||||||
import FormulaFieldAreas from './formula_areas'
|
import FormulaFieldAreas from './formula_areas'
|
||||||
|
import AccessibilityAreas from './accessibility_areas'
|
||||||
import ImageStep from './image_step'
|
import ImageStep from './image_step'
|
||||||
import SignatureStep from './signature_step'
|
import SignatureStep from './signature_step'
|
||||||
import InitialsStep from './initials_step'
|
import InitialsStep from './initials_step'
|
||||||
@@ -642,6 +705,7 @@ export default {
|
|||||||
name: 'SubmissionForm',
|
name: 'SubmissionForm',
|
||||||
components: {
|
components: {
|
||||||
FieldAreas,
|
FieldAreas,
|
||||||
|
AccessibilityAreas,
|
||||||
ImageStep,
|
ImageStep,
|
||||||
SignatureStep,
|
SignatureStep,
|
||||||
AppearsOn,
|
AppearsOn,
|
||||||
@@ -824,6 +888,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
filledFieldsIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
withAccessibilityAreas: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -934,7 +1008,8 @@ export default {
|
|||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
isSubmittingComplete: false,
|
isSubmittingComplete: false,
|
||||||
submittedValues: {},
|
submittedValues: {},
|
||||||
recalculateButtonDisabledKey: ''
|
recalculateButtonDisabledKey: '',
|
||||||
|
isAccessibilityMode: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -1458,7 +1533,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.enableScrollIntoField = false
|
this.enableScrollIntoField = false
|
||||||
this.$refs.form.querySelector('input[type="date"], input[type="number"], input[type="text"], select')?.focus()
|
this.$refs.form.querySelector('input[type="date"], input[type="number"], input[type="text"], input[type="tel"], textarea, select')?.focus()
|
||||||
this.enableScrollIntoField = true
|
this.enableScrollIntoField = true
|
||||||
|
|
||||||
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
|
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
|
||||||
@@ -1629,6 +1704,15 @@ export default {
|
|||||||
|
|
||||||
if (this.completedRedirectUrl) {
|
if (this.completedRedirectUrl) {
|
||||||
window.location.href = sanitizeUrl(this.completedRedirectUrl)
|
window.location.href = sanitizeUrl(this.completedRedirectUrl)
|
||||||
|
} else {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
|
const completedEl = root.getElementById('form_completed')
|
||||||
|
|
||||||
|
if (completedEl) {
|
||||||
|
completedEl.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
step: 'Step',
|
||||||
|
form_progress: 'Form progress',
|
||||||
|
close: 'Close',
|
||||||
|
uploaded_files: 'Uploaded files',
|
||||||
|
signature_drawing_area: 'Signature drawing area. Use mouse or touch to draw your signature.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Please upload an image file',
|
please_upload_an_image_file: 'Please upload an image file',
|
||||||
must_be_characters_length: 'Must be {number} characters long',
|
must_be_characters_length: 'Must be {number} characters long',
|
||||||
@@ -98,10 +103,16 @@ const en = {
|
|||||||
files: 'Files',
|
files: 'Files',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
|
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
|
||||||
browser_privacy_settings_block_canvas: 'Your browser privacy settings restrict use of the drawing canvas. Please use a different browser or device, or disable privacy settings that block canvas in order to sign.',
|
browser_privacy_settings_block_canvas: 'Your browser privacy settings restrict use of the drawing canvas. Please use a different browser or device, or disable privacy settings that block canvas in order to sign.',
|
||||||
wait_countdown_seconds: 'Wait {countdown} seconds'
|
wait_countdown_seconds: 'Wait {countdown} seconds',
|
||||||
|
enter_screen_reader_mode: 'Enter screen reader mode'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
step: 'Paso',
|
||||||
|
form_progress: 'Progreso del formulario',
|
||||||
|
close: 'Cerrar',
|
||||||
|
uploaded_files: 'Archivos subidos',
|
||||||
|
signature_drawing_area: 'Área de dibujo de firma. Use el ratón o toque para dibujar su firma.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
|
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
|
||||||
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
|
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
|
||||||
@@ -201,10 +212,16 @@ const es = {
|
|||||||
files: 'Archivos',
|
files: 'Archivos',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
|
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
|
||||||
browser_privacy_settings_block_canvas: 'La configuración de privacidad de su navegador restringe el uso del lienzo de dibujo. Utilice un navegador o dispositivo diferente, o desactive la configuración de privacidad que bloquea el lienzo para firmar.',
|
browser_privacy_settings_block_canvas: 'La configuración de privacidad de su navegador restringe el uso del lienzo de dibujo. Utilice un navegador o dispositivo diferente, o desactive la configuración de privacidad que bloquea el lienzo para firmar.',
|
||||||
wait_countdown_seconds: 'Espera {countdown} segundos'
|
wait_countdown_seconds: 'Espera {countdown} segundos',
|
||||||
|
enter_screen_reader_mode: 'Activar modo lector de pantalla'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
step: 'Passo',
|
||||||
|
form_progress: 'Progresso del modulo',
|
||||||
|
close: 'Chiudi',
|
||||||
|
uploaded_files: 'File caricati',
|
||||||
|
signature_drawing_area: 'Area di disegno della firma. Usa il mouse o il tocco per disegnare la tua firma.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Per favore carica un file immagine',
|
please_upload_an_image_file: 'Per favore carica un file immagine',
|
||||||
must_be_characters_length: 'Deve essere lungo {number} caratteri',
|
must_be_characters_length: 'Deve essere lungo {number} caratteri',
|
||||||
@@ -304,10 +321,16 @@ const it = {
|
|||||||
files: 'File',
|
files: 'File',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
|
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
|
||||||
browser_privacy_settings_block_canvas: 'Le impostazioni sulla privacy del browser limitano l\'uso dell\'area di disegno. Utilizza un browser o dispositivo diverso oppure disattiva le impostazioni sulla privacy che bloccano il canvas per firmare.',
|
browser_privacy_settings_block_canvas: 'Le impostazioni sulla privacy del browser limitano l\'uso dell\'area di disegno. Utilizza un browser o dispositivo diverso oppure disattiva le impostazioni sulla privacy che bloccano il canvas per firmare.',
|
||||||
wait_countdown_seconds: 'Attendi {countdown} secondi'
|
wait_countdown_seconds: 'Attendi {countdown} secondi',
|
||||||
|
enter_screen_reader_mode: 'Attiva modalità lettore di schermo'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
step: 'Schritt',
|
||||||
|
form_progress: 'Formularfortschritt',
|
||||||
|
close: 'Schließen',
|
||||||
|
uploaded_files: 'Hochgeladene Dateien',
|
||||||
|
signature_drawing_area: 'Unterschriftszeichenbereich. Verwenden Sie die Maus oder Berührung, um Ihre Unterschrift zu zeichnen.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
||||||
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
||||||
@@ -407,10 +430,16 @@ const de = {
|
|||||||
files: 'Dateien',
|
files: 'Dateien',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte neu zeichnen.',
|
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte neu zeichnen.',
|
||||||
browser_privacy_settings_block_canvas: 'Die Datenschutzeinstellungen Ihres Browsers schränken die Nutzung der Zeichenfläche ein. Bitte verwenden Sie einen anderen Browser oder ein anderes Gerät oder deaktivieren Sie die Datenschutzeinstellungen, die Canvas blockieren, um zu unterschreiben.',
|
browser_privacy_settings_block_canvas: 'Die Datenschutzeinstellungen Ihres Browsers schränken die Nutzung der Zeichenfläche ein. Bitte verwenden Sie einen anderen Browser oder ein anderes Gerät oder deaktivieren Sie die Datenschutzeinstellungen, die Canvas blockieren, um zu unterschreiben.',
|
||||||
wait_countdown_seconds: 'Bitte {countdown} Sekunden warten'
|
wait_countdown_seconds: 'Bitte {countdown} Sekunden warten',
|
||||||
|
enter_screen_reader_mode: 'Screenreader-Modus aktivieren'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
step: 'Étape',
|
||||||
|
form_progress: 'Progression du formulaire',
|
||||||
|
close: 'Fermer',
|
||||||
|
uploaded_files: 'Fichiers téléchargés',
|
||||||
|
signature_drawing_area: 'Zone de dessin de signature. Utilisez la souris ou le toucher pour dessiner votre signature.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Veuillez téléverser un fichier image',
|
please_upload_an_image_file: 'Veuillez téléverser un fichier image',
|
||||||
must_be_characters_length: 'Doit comporter {number} caractères',
|
must_be_characters_length: 'Doit comporter {number} caractères',
|
||||||
@@ -510,10 +539,16 @@ const fr = {
|
|||||||
files: 'Fichiers',
|
files: 'Fichiers',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
|
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
|
||||||
browser_privacy_settings_block_canvas: 'Les paramètres de confidentialité de votre navigateur empêchent l\'utilisation du canevas de dessin. Veuillez utiliser un autre navigateur ou appareil, ou désactiver les paramètres de confidentialité qui bloquent le canevas pour signer.',
|
browser_privacy_settings_block_canvas: 'Les paramètres de confidentialité de votre navigateur empêchent l\'utilisation du canevas de dessin. Veuillez utiliser un autre navigateur ou appareil, ou désactiver les paramètres de confidentialité qui bloquent le canevas pour signer.',
|
||||||
wait_countdown_seconds: 'Veuillez patienter {countdown} secondes'
|
wait_countdown_seconds: 'Veuillez patienter {countdown} secondes',
|
||||||
|
enter_screen_reader_mode: 'Activer le mode lecteur d\'écran'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
|
step: 'Krok',
|
||||||
|
form_progress: 'Postęp formularza',
|
||||||
|
close: 'Zamknij',
|
||||||
|
uploaded_files: 'Przesłane pliki',
|
||||||
|
signature_drawing_area: 'Obszar rysowania podpisu. Użyj myszy lub dotyku, aby narysować swój podpis.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
|
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
|
||||||
must_be_characters_length: 'Musi mieć długość {number} znaków',
|
must_be_characters_length: 'Musi mieć długość {number} znaków',
|
||||||
@@ -613,10 +648,16 @@ const pl = {
|
|||||||
files: 'Pliki',
|
files: 'Pliki',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.',
|
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.',
|
||||||
browser_privacy_settings_block_canvas: 'Ustawienia prywatności przeglądarki blokują użycie obszaru rysowania. Użyj innej przeglądarki lub urządzenia albo wyłącz ustawienia prywatności blokujące canvas, aby podpisać.',
|
browser_privacy_settings_block_canvas: 'Ustawienia prywatności przeglądarki blokują użycie obszaru rysowania. Użyj innej przeglądarki lub urządzenia albo wyłącz ustawienia prywatności blokujące canvas, aby podpisać.',
|
||||||
wait_countdown_seconds: 'Poczekaj {countdown} sekund'
|
wait_countdown_seconds: 'Poczekaj {countdown} sekund',
|
||||||
|
enter_screen_reader_mode: 'Włącz tryb czytnika ekranu'
|
||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
|
step: 'Крок',
|
||||||
|
form_progress: 'Прогрес форми',
|
||||||
|
close: 'Закрити',
|
||||||
|
uploaded_files: 'Завантажені файли',
|
||||||
|
signature_drawing_area: 'Область малювання підпису. Використовуйте мишу або дотик, щоб намалювати свій підпис.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
|
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
|
||||||
must_be_characters_length: 'Має містити {number} символів',
|
must_be_characters_length: 'Має містити {number} символів',
|
||||||
@@ -716,10 +757,16 @@ const uk = {
|
|||||||
files: 'Файли',
|
files: 'Файли',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
|
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
|
||||||
browser_privacy_settings_block_canvas: 'Налаштування конфіденційності вашого браузера блокують використання полотна для малювання. Будь ласка, скористайтеся іншим браузером або пристроєм, або вимкніть налаштування конфіденційності, що блокують canvas, щоб підписати.',
|
browser_privacy_settings_block_canvas: 'Налаштування конфіденційності вашого браузера блокують використання полотна для малювання. Будь ласка, скористайтеся іншим браузером або пристроєм, або вимкніть налаштування конфіденційності, що блокують canvas, щоб підписати.',
|
||||||
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
|
wait_countdown_seconds: 'Зачекайте {countdown} секунд',
|
||||||
|
enter_screen_reader_mode: 'Увімкнути режим читання з екрану'
|
||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
|
step: 'Krok',
|
||||||
|
form_progress: 'Průběh formuláře',
|
||||||
|
close: 'Zavřít',
|
||||||
|
uploaded_files: 'Nahrané soubory',
|
||||||
|
signature_drawing_area: 'Oblast pro kreslení podpisu. Použijte myš nebo dotyk k nakreslení podpisu.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
|
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
|
||||||
must_be_characters_length: 'Musí mít délku {number} znaků',
|
must_be_characters_length: 'Musí mít délku {number} znaků',
|
||||||
@@ -819,10 +866,16 @@ const cs = {
|
|||||||
files: 'Soubory',
|
files: 'Soubory',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
|
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
|
||||||
browser_privacy_settings_block_canvas: 'Nastavení soukromí vašeho prohlížeče omezuje použití kreslicího plátna. Použijte prosím jiný prohlížeč nebo zařízení, nebo vypněte nastavení soukromí blokující canvas pro podepsání.',
|
browser_privacy_settings_block_canvas: 'Nastavení soukromí vašeho prohlížeče omezuje použití kreslicího plátna. Použijte prosím jiný prohlížeč nebo zařízení, nebo vypněte nastavení soukromí blokující canvas pro podepsání.',
|
||||||
wait_countdown_seconds: 'Počkejte {countdown} sekund'
|
wait_countdown_seconds: 'Počkejte {countdown} sekund',
|
||||||
|
enter_screen_reader_mode: 'Zapnout režim čtečky obrazovky'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
step: 'Passo',
|
||||||
|
form_progress: 'Progresso do formulário',
|
||||||
|
close: 'Fechar',
|
||||||
|
uploaded_files: 'Arquivos enviados',
|
||||||
|
signature_drawing_area: 'Área de desenho da assinatura. Use o mouse ou toque para desenhar sua assinatura.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
|
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
|
||||||
must_be_characters_length: 'Deve ter {number} caracteres',
|
must_be_characters_length: 'Deve ter {number} caracteres',
|
||||||
@@ -922,10 +975,16 @@ const pt = {
|
|||||||
files: 'Arquivos',
|
files: 'Arquivos',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
|
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
|
||||||
browser_privacy_settings_block_canvas: 'As configurações de privacidade do seu navegador restringem o uso da área de desenho. Use um navegador ou dispositivo diferente, ou desative as configurações de privacidade que bloqueiam o canvas para assinar.',
|
browser_privacy_settings_block_canvas: 'As configurações de privacidade do seu navegador restringem o uso da área de desenho. Use um navegador ou dispositivo diferente, ou desative as configurações de privacidade que bloqueiam o canvas para assinar.',
|
||||||
wait_countdown_seconds: 'Aguarde {countdown} segundos'
|
wait_countdown_seconds: 'Aguarde {countdown} segundos',
|
||||||
|
enter_screen_reader_mode: 'Ativar modo leitor de tela'
|
||||||
}
|
}
|
||||||
|
|
||||||
const he = {
|
const he = {
|
||||||
|
step: 'שלב',
|
||||||
|
form_progress: 'התקדמות הטופס',
|
||||||
|
close: 'סגור',
|
||||||
|
uploaded_files: 'קבצים שהועלו',
|
||||||
|
signature_drawing_area: 'אזור ציור חתימה. השתמש בעכבר או במגע כדי לצייר את החתימה שלך.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
|
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
|
||||||
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
|
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
|
||||||
@@ -1025,10 +1084,16 @@ const he = {
|
|||||||
files: 'קבצים',
|
files: 'קבצים',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
|
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
|
||||||
browser_privacy_settings_block_canvas: 'הגדרות הפרטיות של הדפדפן שלך מגבילות את השימוש באזור הציור. אנא השתמש בדפדפן או מכשיר אחר, או בטל את הגדרות הפרטיות החוסמות canvas כדי לחתום.',
|
browser_privacy_settings_block_canvas: 'הגדרות הפרטיות של הדפדפן שלך מגבילות את השימוש באזור הציור. אנא השתמש בדפדפן או מכשיר אחר, או בטל את הגדרות הפרטיות החוסמות canvas כדי לחתום.',
|
||||||
wait_countdown_seconds: 'המתן {countdown} שניות'
|
wait_countdown_seconds: 'המתן {countdown} שניות',
|
||||||
|
enter_screen_reader_mode: 'הפעל מצב קורא מסך'
|
||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
step: 'Stap',
|
||||||
|
form_progress: 'Formuliervoortgang',
|
||||||
|
close: 'Sluiten',
|
||||||
|
uploaded_files: 'Geüploade bestanden',
|
||||||
|
signature_drawing_area: 'Handtekening tekengebied. Gebruik de muis of aanraking om uw handtekening te tekenen.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
|
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
|
||||||
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
||||||
@@ -1128,10 +1193,16 @@ const nl = {
|
|||||||
files: 'Bestanden',
|
files: 'Bestanden',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
|
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
|
||||||
browser_privacy_settings_block_canvas: 'De privacyinstellingen van uw browser beperken het gebruik van het tekenveld. Gebruik een andere browser of ander apparaat, of schakel de privacyinstellingen uit die canvas blokkeren om te ondertekenen.',
|
browser_privacy_settings_block_canvas: 'De privacyinstellingen van uw browser beperken het gebruik van het tekenveld. Gebruik een andere browser of ander apparaat, of schakel de privacyinstellingen uit die canvas blokkeren om te ondertekenen.',
|
||||||
wait_countdown_seconds: 'Wacht {countdown} seconden'
|
wait_countdown_seconds: 'Wacht {countdown} seconden',
|
||||||
|
enter_screen_reader_mode: 'Schermlezer-modus inschakelen'
|
||||||
}
|
}
|
||||||
|
|
||||||
const ar = {
|
const ar = {
|
||||||
|
step: 'خطوة',
|
||||||
|
form_progress: 'تقدم النموذج',
|
||||||
|
close: 'إغلاق',
|
||||||
|
uploaded_files: 'الملفات المرفوعة',
|
||||||
|
signature_drawing_area: 'منطقة رسم التوقيع. استخدم الماوس أو اللمس لرسم توقيعك.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
|
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
|
||||||
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
|
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
|
||||||
@@ -1231,10 +1302,16 @@ const ar = {
|
|||||||
files: 'الملفات',
|
files: 'الملفات',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
|
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
|
||||||
browser_privacy_settings_block_canvas: 'إعدادات الخصوصية في متصفحك تمنع استخدام لوحة الرسم. يرجى استخدام متصفح أو جهاز مختلف، أو تعطيل إعدادات الخصوصية التي تحظر canvas للتوقيع.',
|
browser_privacy_settings_block_canvas: 'إعدادات الخصوصية في متصفحك تمنع استخدام لوحة الرسم. يرجى استخدام متصفح أو جهاز مختلف، أو تعطيل إعدادات الخصوصية التي تحظر canvas للتوقيع.',
|
||||||
wait_countdown_seconds: 'انتظر {countdown} ثانية'
|
wait_countdown_seconds: 'انتظر {countdown} ثانية',
|
||||||
|
enter_screen_reader_mode: 'تفعيل وضع قارئ الشاشة'
|
||||||
}
|
}
|
||||||
|
|
||||||
const ko = {
|
const ko = {
|
||||||
|
step: '단계',
|
||||||
|
form_progress: '양식 진행',
|
||||||
|
close: '닫기',
|
||||||
|
uploaded_files: '업로드된 파일',
|
||||||
|
signature_drawing_area: '서명 그리기 영역. 마우스 또는 터치를 사용하여 서명을 그리세요.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
|
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
|
||||||
must_be_characters_length: '{number}자여야 합니다',
|
must_be_characters_length: '{number}자여야 합니다',
|
||||||
@@ -1334,10 +1411,16 @@ const ko = {
|
|||||||
files: '파일',
|
files: '파일',
|
||||||
signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
|
signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
|
||||||
browser_privacy_settings_block_canvas: '브라우저 개인정보 보호 설정으로 인해 그리기 캔버스를 사용할 수 없습니다. 다른 브라우저나 기기를 사용하거나, 서명을 위해 캔버스를 차단하는 개인정보 보호 설정을 비활성화해 주세요.',
|
browser_privacy_settings_block_canvas: '브라우저 개인정보 보호 설정으로 인해 그리기 캔버스를 사용할 수 없습니다. 다른 브라우저나 기기를 사용하거나, 서명을 위해 캔버스를 차단하는 개인정보 보호 설정을 비활성화해 주세요.',
|
||||||
wait_countdown_seconds: '{countdown}초 기다리세요'
|
wait_countdown_seconds: '{countdown}초 기다리세요',
|
||||||
|
enter_screen_reader_mode: '스크린 리더 모드 활성화'
|
||||||
}
|
}
|
||||||
|
|
||||||
const ja = {
|
const ja = {
|
||||||
|
step: 'ステップ',
|
||||||
|
form_progress: 'フォームの進捗',
|
||||||
|
close: '閉じる',
|
||||||
|
uploaded_files: 'アップロードされたファイル',
|
||||||
|
signature_drawing_area: '署名描画エリア。マウスまたはタッチを使用して署名を描いてください。',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: '画像ファイルをアップロードしてください',
|
please_upload_an_image_file: '画像ファイルをアップロードしてください',
|
||||||
must_be_characters_length: '{number}文字でなければなりません',
|
must_be_characters_length: '{number}文字でなければなりません',
|
||||||
@@ -1437,7 +1520,8 @@ const ja = {
|
|||||||
files: 'ファイル',
|
files: 'ファイル',
|
||||||
signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。',
|
signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。',
|
||||||
browser_privacy_settings_block_canvas: 'ブラウザのプライバシー設定により、描画キャンバスの使用が制限されています。別のブラウザまたはデバイスを使用するか、署名するためにキャンバスをブロックするプライバシー設定を無効にしてください。',
|
browser_privacy_settings_block_canvas: 'ブラウザのプライバシー設定により、描画キャンバスの使用が制限されています。別のブラウザまたはデバイスを使用するか、署名するためにキャンバスをブロックするプライバシー設定を無効にしてください。',
|
||||||
wait_countdown_seconds: '{countdown} 秒お待ちください'
|
wait_countdown_seconds: '{countdown} 秒お待ちください',
|
||||||
|
enter_screen_reader_mode: 'スクリーンリーダーモードを有効にする'
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko, ja }
|
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko, ja }
|
||||||
|
|||||||
@@ -18,13 +18,17 @@
|
|||||||
class="btn btn-outline btn-sm reupload-button"
|
class="btn btn-outline btn-sm reupload-button"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload
|
||||||
|
:width="16"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
{{ t('reupload') }}
|
{{ t('reupload') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
:src="attachmentsIndex[modelValue].url"
|
:src="attachmentsIndex[modelValue].url"
|
||||||
|
:alt="field.name || t('image')"
|
||||||
class="h-52 border border-base-300 rounded mx-auto uploaded-image-preview"
|
class="h-52 border border-base-300 rounded mx-auto uploaded-image-preview"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,43 +22,60 @@
|
|||||||
class="md:tooltip"
|
class="md:tooltip"
|
||||||
:data-tip="t('type_initial')"
|
:data-tip="t('type_initial')"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
type="button"
|
||||||
|
:aria-label="t('type_initial')"
|
||||||
class="btn btn-outline font-medium btn-sm type-text-button"
|
class="btn btn-outline font-medium btn-sm type-text-button"
|
||||||
@click.prevent="toggleTextInput"
|
@click="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconTextSize :width="16" />
|
<IconTextSize
|
||||||
|
:width="16"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
<span class="hidden sm:inline">
|
<span class="hidden sm:inline">
|
||||||
{{ t('type') }}
|
{{ t('type') }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="md:tooltip ml-2"
|
class="md:tooltip ml-2"
|
||||||
:data-tip="t('draw_initials')"
|
:data-tip="t('draw_initials')"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
type="button"
|
||||||
|
:aria-label="t('draw_initials')"
|
||||||
class="btn btn-outline font-medium btn-sm type-text-button"
|
class="btn btn-outline font-medium btn-sm type-text-button"
|
||||||
@click.prevent="toggleTextInput"
|
@click="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconSignature :width="16" />
|
<IconSignature
|
||||||
|
:width="16"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
<span class="hidden sm:inline">
|
<span class="hidden sm:inline">
|
||||||
{{ t('draw') }}
|
{{ t('draw') }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="md:tooltip"
|
class="md:tooltip"
|
||||||
:data-tip="t('click_to_upload')"
|
:data-tip="t('click_to_upload')"
|
||||||
>
|
>
|
||||||
<label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
|
<button
|
||||||
<IconUpload :width="16" />
|
type="button"
|
||||||
|
:aria-label="t('click_to_upload')"
|
||||||
|
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button"
|
||||||
|
@click="$refs.uploadInput.click()"
|
||||||
|
>
|
||||||
|
<IconUpload
|
||||||
|
:width="16"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
:key="uploadImageInputKey"
|
:key="uploadImageInputKey"
|
||||||
|
ref="uploadInput"
|
||||||
type="file"
|
type="file"
|
||||||
hidden
|
hidden
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -67,37 +84,45 @@
|
|||||||
<span class="hidden sm:inline">
|
<span class="hidden sm:inline">
|
||||||
{{ t('upload') }}
|
{{ t('upload') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<a
|
<button
|
||||||
v-if="modelValue || computedPreviousValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
href="#"
|
type="button"
|
||||||
class="btn font-medium btn-outline btn-sm clear-canvas-button"
|
class="btn font-medium btn-outline btn-sm clear-canvas-button"
|
||||||
@click.prevent="remove"
|
@click="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload
|
||||||
|
:width="16"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</a>
|
</button>
|
||||||
<a
|
<button
|
||||||
v-else
|
v-else
|
||||||
href="#"
|
type="button"
|
||||||
class="btn font-medium btn-outline btn-sm clear-canvas-button"
|
class="btn font-medium btn-outline btn-sm clear-canvas-button"
|
||||||
@click.prevent="clear"
|
@click="clear"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload
|
||||||
|
:width="16"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</a>
|
</button>
|
||||||
<a
|
<button
|
||||||
|
type="button"
|
||||||
:title="t('minimize')"
|
:title="t('minimize')"
|
||||||
href="#"
|
:aria-label="t('minimize')"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click="$emit('minimize')"
|
||||||
>
|
>
|
||||||
<IconArrowsDiagonalMinimize2
|
<IconArrowsDiagonalMinimize2
|
||||||
:width="20"
|
:width="20"
|
||||||
:height="20"
|
:height="20"
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
|
:id="field.uuid + '-desc'"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1 field-description-text"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
:name="`values[${field.uuid}][]`"
|
:name="`values[${field.uuid}][]`"
|
||||||
:value="optionValue(option, index)"
|
:value="optionValue(option, index)"
|
||||||
|
:aria-describedby="field.description ? field.uuid + '-desc' : undefined"
|
||||||
class="base-checkbox !h-7 !w-7"
|
class="base-checkbox !h-7 !w-7"
|
||||||
:checked="(modelValue || []).includes(optionValue(option, index))"
|
:checked="(modelValue || []).includes(optionValue(option, index))"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
|
:id="field.uuid + '-desc'"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1 field-description-text"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
:max="field.validation?.max"
|
:max="field.validation?.max"
|
||||||
class="base-input !text-2xl w-full"
|
class="base-input !text-2xl w-full"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
|
:aria-describedby="field.description ? field.uuid + '-desc' : undefined"
|
||||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
|
:id="field.uuid + '-desc'"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1 field-description-text"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
@@ -42,28 +43,28 @@
|
|||||||
@input="onInputCode"
|
@input="onInputCode"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between mt-2 -mb-2 md:-mb-4">
|
<div class="flex justify-between mt-2 -mb-2 md:-mb-4">
|
||||||
<a
|
<button
|
||||||
v-if="!defaultValue"
|
v-if="!defaultValue"
|
||||||
href="#"
|
type="button"
|
||||||
class="link change-phone-number-link"
|
class="link change-phone-number-link"
|
||||||
@click.prevent="isCodeSent = false"
|
@click="isCodeSent = false"
|
||||||
>
|
>
|
||||||
{{ t('change_phone_number') }}
|
{{ t('change_phone_number') }}
|
||||||
</a>
|
</button>
|
||||||
<span
|
<span
|
||||||
v-if="resendCodeCountdown > 0"
|
v-if="resendCodeCountdown > 0"
|
||||||
class="link"
|
class="link"
|
||||||
>
|
>
|
||||||
{{ t('wait_countdown_seconds').replace('{countdown}', resendCodeCountdown) }}
|
{{ t('wait_countdown_seconds').replace('{countdown}', resendCodeCountdown) }}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<button
|
||||||
v-else
|
v-else
|
||||||
href="#"
|
type="button"
|
||||||
class="link resend-code-link"
|
class="link resend-code-link"
|
||||||
@click.prevent="resendCode"
|
@click="resendCode"
|
||||||
>
|
>
|
||||||
{{ isResendLoading ? t('sending') : t('resend_code') }}
|
{{ isResendLoading ? t('sending') : t('resend_code') }}
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
type="tel"
|
type="tel"
|
||||||
inputmode="tel"
|
inputmode="tel"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
|
:aria-describedby="field.description ? field.uuid + '-desc' : undefined"
|
||||||
placeholder="234 567-8900"
|
placeholder="234 567-8900"
|
||||||
@input="onPhoneInput"
|
@input="onPhoneInput"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
|
|||||||
@@ -25,17 +25,21 @@
|
|||||||
class="md:tooltip"
|
class="md:tooltip"
|
||||||
:data-tip="t('draw_signature')"
|
:data-tip="t('draw_signature')"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
type="button"
|
||||||
|
:aria-label="t('draw_signature')"
|
||||||
class="btn btn-outline btn-sm font-medium type-text-button"
|
class="btn btn-outline btn-sm font-medium type-text-button"
|
||||||
@click.prevent="[toggleTextInput(), hideQr()]"
|
@click="[toggleTextInput(), hideQr()]"
|
||||||
>
|
>
|
||||||
<IconSignature :width="16" />
|
<IconSignature
|
||||||
|
:width="16"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
<span class="hidden sm:inline">
|
<span class="hidden sm:inline">
|
||||||
{{ t('draw') }}
|
{{ t('draw') }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="withTypedSignature && format !== 'drawn_or_upload' && format !== 'typed_or_upload' && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
|
v-else-if="withTypedSignature && format !== 'drawn_or_upload' && format !== 'typed_or_upload' && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
|
||||||
@@ -43,17 +47,21 @@
|
|||||||
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
||||||
:data-tip="t('type_text')"
|
:data-tip="t('type_text')"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
type="button"
|
||||||
|
:aria-label="t('type_text')"
|
||||||
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap type-text-button"
|
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap type-text-button"
|
||||||
@click.prevent="[toggleTextInput(), hideQr()]"
|
@click="[toggleTextInput(), hideQr()]"
|
||||||
>
|
>
|
||||||
<IconTextSize :width="16" />
|
<IconTextSize
|
||||||
|
:width="16"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
<span class="hidden sm:inline">
|
<span class="hidden sm:inline">
|
||||||
{{ t('type') }}
|
{{ t('type') }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'"
|
v-if="format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'"
|
||||||
@@ -61,10 +69,19 @@
|
|||||||
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
||||||
:data-tip="t('take_photo')"
|
:data-tip="t('take_photo')"
|
||||||
>
|
>
|
||||||
<label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
|
<button
|
||||||
<IconCamera :width="16" />
|
type="button"
|
||||||
|
:aria-label="t('take_photo')"
|
||||||
|
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button"
|
||||||
|
@click="$refs.takePhotoInput.click()"
|
||||||
|
>
|
||||||
|
<IconCamera
|
||||||
|
:width="16"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
:key="uploadImageInputKey"
|
:key="uploadImageInputKey"
|
||||||
|
ref="takePhotoInput"
|
||||||
type="file"
|
type="file"
|
||||||
hidden
|
hidden
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -73,49 +90,57 @@
|
|||||||
<span class="hidden sm:inline">
|
<span class="hidden sm:inline">
|
||||||
{{ t('upload') }}
|
{{ t('upload') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<a
|
<button
|
||||||
v-if="modelValue || computedPreviousValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
href="#"
|
type="button"
|
||||||
class="btn btn-outline btn-sm font-medium reupload-button"
|
class="btn btn-outline btn-sm font-medium reupload-button"
|
||||||
@click.prevent="remove"
|
@click="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload
|
||||||
|
:width="16"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
{{ t(format === 'upload' ? 'reupload' : 'redraw') }}
|
{{ t(format === 'upload' ? 'reupload' : 'redraw') }}
|
||||||
</a>
|
</button>
|
||||||
<span
|
<span
|
||||||
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
|
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
|
||||||
class="md:tooltip before:translate-x-[-90%]"
|
class="md:tooltip before:translate-x-[-90%]"
|
||||||
:data-tip="t('sign_on_the_touchscreen')"
|
:data-tip="t('sign_on_the_touchscreen')"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
href="#"
|
type="button"
|
||||||
|
:aria-label="t('sign_on_the_touchscreen')"
|
||||||
class="btn btn-sm btn-neutral font-medium hidden md:flex"
|
class="btn btn-sm btn-neutral font-medium hidden md:flex"
|
||||||
:class="{ 'btn-outline': !isShowQr, 'text-white': isShowQr }"
|
:class="{ 'btn-outline': !isShowQr, 'text-white': isShowQr }"
|
||||||
@click.prevent="isShowQr ? hideQr() : [isTextSignature = false, showQr()]"
|
@click="isShowQr ? hideQr() : [isTextSignature = false, showQr()]"
|
||||||
>
|
>
|
||||||
<IconQrcode
|
<IconQrcode
|
||||||
:width="19"
|
:width="19"
|
||||||
:height="19"
|
:height="19"
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</a>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<a
|
<button
|
||||||
href="#"
|
type="button"
|
||||||
:title="t('minimize')"
|
:title="t('minimize')"
|
||||||
|
:aria-label="t('minimize')"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click="$emit('minimize')"
|
||||||
>
|
>
|
||||||
<IconArrowsDiagonalMinimize2
|
<IconArrowsDiagonalMinimize2
|
||||||
:width="20"
|
:width="20"
|
||||||
:height="20"
|
:height="20"
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
|
:id="field.uuid + '-desc'"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1 field-description-text"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
@@ -136,6 +161,7 @@
|
|||||||
<img
|
<img
|
||||||
v-if="modelValue || computedPreviousValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||||
|
:alt="field.name || t('signature')"
|
||||||
class="mx-auto bg-white border border-base-300 rounded max-h-44"
|
class="mx-auto bg-white border border-base-300 rounded max-h-44"
|
||||||
>
|
>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
@@ -154,14 +180,17 @@
|
|||||||
v-if="!modelValue && !computedPreviousValue && !isShowQr && !isTextSignature && isSignatureStarted"
|
v-if="!modelValue && !computedPreviousValue && !isShowQr && !isTextSignature && isSignatureStarted"
|
||||||
class="absolute top-0.5 right-0.5"
|
class="absolute top-0.5 right-0.5"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
href="#"
|
type="button"
|
||||||
class="btn btn-ghost font-medium btn-xs md:btn-sm"
|
class="btn btn-ghost font-medium btn-xs md:btn-sm"
|
||||||
@click.prevent="[clear(), hideQr()]"
|
@click="[clear(), hideQr()]"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload
|
||||||
|
:width="16"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isTextSignature"
|
v-if="isTextSignature"
|
||||||
@@ -170,6 +199,8 @@
|
|||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
|
role="application"
|
||||||
|
:aria-label="t('signature_drawing_area')"
|
||||||
style="padding: 1px; 0"
|
style="padding: 1px; 0"
|
||||||
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
|
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
|
||||||
/>
|
/>
|
||||||
@@ -182,13 +213,14 @@
|
|||||||
class="top-0 bottom-0 right-0 left-0 absolute bg-base-content/10 rounded-2xl"
|
class="top-0 bottom-0 right-0 left-0 absolute bg-base-content/10 rounded-2xl"
|
||||||
>
|
>
|
||||||
<div class="absolute top-1.5 right-1.5">
|
<div class="absolute top-1.5 right-1.5">
|
||||||
<a
|
<button
|
||||||
href="#"
|
type="button"
|
||||||
class="btn btn-sm btn-circle btn-normal btn-outline"
|
class="btn btn-sm btn-circle btn-normal btn-outline"
|
||||||
@click.prevent="hideQr"
|
:aria-label="t('close')"
|
||||||
|
@click="hideQr"
|
||||||
>
|
>
|
||||||
<IconX />
|
<IconX aria-hidden="true" />
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center w-full h-full p-4">
|
<div class="flex items-center justify-center w-full h-full p-4">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
|
:id="field.uuid + '-desc'"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1 field-description-text"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
:class="{ '!pr-11 -mr-10': !field.validation?.pattern }"
|
:class="{ '!pr-11 -mr-10': !field.validation?.pattern }"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
:pattern="field.validation?.pattern"
|
:pattern="field.validation?.pattern"
|
||||||
|
:aria-describedby="field.description ? field.uuid + '-desc' : undefined"
|
||||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
type="text"
|
type="text"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
@@ -54,6 +56,7 @@
|
|||||||
v-model="text"
|
v-model="text"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="base-textarea !text-2xl w-full"
|
class="base-textarea !text-2xl w-full"
|
||||||
|
:aria-describedby="field.description ? field.uuid + '-desc' : undefined"
|
||||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
@@ -65,13 +68,14 @@
|
|||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('toggle_multiline_text')"
|
:data-tip="t('toggle_multiline_text')"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
href="#"
|
type="button"
|
||||||
|
:aria-label="t('toggle_multiline_text')"
|
||||||
class="btn btn-ghost btn-circle btn-sm toggle-multiline-text-button"
|
class="btn btn-ghost btn-circle btn-sm toggle-multiline-text-button"
|
||||||
@click.prevent="toggleTextArea"
|
@click="toggleTextArea"
|
||||||
>
|
>
|
||||||
<IconAlignBoxLeftTop />
|
<IconAlignBoxLeftTop aria-hidden="true" />
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
<IconInnerShadowTop
|
<IconInnerShadowTop
|
||||||
width="40"
|
width="40"
|
||||||
class="animate-spin h-10"
|
class="animate-spin h-10"
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="redirectUrl">
|
<div v-else-if="redirectUrl">
|
||||||
|
|||||||
@@ -1097,6 +1097,16 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const deduplicateUuidsIndex = {}
|
||||||
|
|
||||||
|
this.template.submitters.forEach((submitter) => {
|
||||||
|
if (deduplicateUuidsIndex[submitter.uuid]) {
|
||||||
|
submitter.uuid = v4()
|
||||||
|
}
|
||||||
|
|
||||||
|
deduplicateUuidsIndex[submitter.uuid] = true
|
||||||
|
})
|
||||||
|
|
||||||
this.selectedSubmitter = this.template.submitters[0]
|
this.selectedSubmitter = this.template.submitters[0]
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ tiptapStylesheet.replaceSync(
|
|||||||
-webkit-font-variant-ligatures: none;
|
-webkit-font-variant-ligatures: none;
|
||||||
font-variant-ligatures: none;
|
font-variant-ligatures: none;
|
||||||
font-feature-settings: "liga" 0;
|
font-feature-settings: "liga" 0;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
min-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror > article {
|
||||||
|
margin-bottom: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror [contenteditable="false"] {
|
.ProseMirror [contenteditable="false"] {
|
||||||
@@ -188,6 +195,8 @@ const CustomHeading = Node.create({
|
|||||||
|
|
||||||
const SectionNode = createBlockNode('section', 'section')
|
const SectionNode = createBlockNode('section', 'section')
|
||||||
const ArticleNode = createBlockNode('article', 'article')
|
const ArticleNode = createBlockNode('article', 'article')
|
||||||
|
const HeaderNode = createBlockNode('header', 'header')
|
||||||
|
const FooterNode = createBlockNode('footer', 'footer')
|
||||||
const DivNode = createBlockNode('div', 'div')
|
const DivNode = createBlockNode('div', 'div')
|
||||||
const BlockquoteNode = createBlockNode('blockquote', 'blockquote')
|
const BlockquoteNode = createBlockNode('blockquote', 'blockquote')
|
||||||
const PreNode = createBlockNode('pre', 'pre')
|
const PreNode = createBlockNode('pre', 'pre')
|
||||||
@@ -738,14 +747,12 @@ export function buildEditor ({ dynamicAreaProps, attachmentsIndex, renderHtmlFor
|
|||||||
History,
|
History,
|
||||||
Gapcursor,
|
Gapcursor,
|
||||||
Dropcursor,
|
Dropcursor,
|
||||||
CustomBold,
|
|
||||||
CustomItalic,
|
|
||||||
CustomUnderline,
|
|
||||||
CustomStrike,
|
|
||||||
CustomParagraph,
|
CustomParagraph,
|
||||||
CustomHeading,
|
CustomHeading,
|
||||||
SectionNode,
|
SectionNode,
|
||||||
ArticleNode,
|
ArticleNode,
|
||||||
|
HeaderNode,
|
||||||
|
FooterNode,
|
||||||
DivNode,
|
DivNode,
|
||||||
BlockquoteNode,
|
BlockquoteNode,
|
||||||
PreNode,
|
PreNode,
|
||||||
@@ -764,6 +771,10 @@ export function buildEditor ({ dynamicAreaProps, attachmentsIndex, renderHtmlFor
|
|||||||
EmptySpanNode,
|
EmptySpanNode,
|
||||||
LinkMark,
|
LinkMark,
|
||||||
SpanMark,
|
SpanMark,
|
||||||
|
CustomBold,
|
||||||
|
CustomItalic,
|
||||||
|
CustomUnderline,
|
||||||
|
CustomStrike,
|
||||||
SubscriptMark,
|
SubscriptMark,
|
||||||
SuperscriptMark,
|
SuperscriptMark,
|
||||||
VariableHighlight,
|
VariableHighlight,
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: document_metadata
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# blob_checksum :string not null
|
||||||
|
# text_runs :text not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# account_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_document_metadata_on_account_id_and_blob_checksum (account_id,blob_checksum) UNIQUE
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (account_id => accounts.id)
|
||||||
|
#
|
||||||
|
class DocumentMetadata < ApplicationRecord
|
||||||
|
belongs_to :account
|
||||||
|
|
||||||
|
attribute :text_runs, :string, default: -> { {} }
|
||||||
|
|
||||||
|
serialize :text_runs, coder: JSON
|
||||||
|
end
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
|
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
|
||||||
<path d="M9 12l2 2l4 -4"></path>
|
<path d="M9 12l2 2l4 -4"></path>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 389 B After Width: | Height: | Size: 408 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2" />
|
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2" />
|
||||||
<path d="M7 11l5 5l5 -5" />
|
<path d="M7 11l5 5l5 -5" />
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 397 B After Width: | Height: | Size: 416 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path>
|
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path>
|
||||||
<path d="M12 9h.01"></path>
|
<path d="M12 9h.01"></path>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 412 B After Width: | Height: | Size: 431 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M12 3a9 9 0 1 0 9 9" />
|
<path d="M12 3a9 9 0 1 0 9 9" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 318 B After Width: | Height: | Size: 337 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M12 18h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v7.5" />
|
<path d="M12 18h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v7.5" />
|
||||||
<path d="M3 6l9 6l9 -6" />
|
<path d="M3 6l9 6l9 -6" />
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 448 B After Width: | Height: | Size: 467 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="<%= local_assigns[:class] %>">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M17 17h2a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-14a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h2" />
|
||||||
|
<path d="M17 9v-4a2 2 0 0 0 -2 -2h-6a2 2 0 0 0 -2 2v4" />
|
||||||
|
<path d="M7 15a2 2 0 0 1 2 -2h6a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-6a2 2 0 0 1 -2 -2l0 -4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 529 B |
@@ -0,0 +1,15 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="<%= local_assigns[:class] %>">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M4 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||||
|
<path d="M7 17l0 .01" />
|
||||||
|
<path d="M14 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||||
|
<path d="M7 7l0 .01" />
|
||||||
|
<path d="M4 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||||
|
<path d="M17 7l0 .01" />
|
||||||
|
<path d="M14 14l3 0" />
|
||||||
|
<path d="M20 14l0 .01" />
|
||||||
|
<path d="M14 14l0 3" />
|
||||||
|
<path d="M14 20l3 0" />
|
||||||
|
<path d="M17 17l3 0" />
|
||||||
|
<path d="M20 17l0 3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 797 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
<path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path>
|
<path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path>
|
||||||
<path d="M20 4v5h-5"></path>
|
<path d="M20 4v5h-5"></path>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 409 B After Width: | Height: | Size: 428 B |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="<%= local_assigns[:class] %>"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" /><path d="M6 21v-2a4 4 0 0 1 4 -4h3" /><path d="M16 22l5 -5" /><path d="M21 21.5v-4.5h-4.5" /></svg>
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="<%= local_assigns[:class] %>"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" /><path d="M6 21v-2a4 4 0 0 1 4 -4h3" /><path d="M16 22l5 -5" /><path d="M21 21.5v-4.5h-4.5" /></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 435 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
<path d="M3 19c3.333 -2 5 -4 5 -6c0 -3 -1 -3 -2 -3s-2.032 1.085 -2 3c.034 2.048 1.658 2.877 2.5 4c1.5 2 2.5 2.5 3.5 1c.667 -1 1.167 -1.833 1.5 -2.5c1 2.333 2.333 3.5 4 3.5h2.5"></path>
|
<path d="M3 19c3.333 -2 5 -4 5 -6c0 -3 -1 -3 -2 -3s-2.032 1.085 -2 3c.034 2.048 1.658 2.877 2.5 4c1.5 2 2.5 2.5 3.5 1c.667 -1 1.167 -1.833 1.5 -2.5c1 2.333 2.333 3.5 4 3.5h2.5"></path>
|
||||||
<path d="M20 17v-12c0 -1.121 -.879 -2 -2 -2s-2 .879 -2 2v12l2 2l2 -2z"></path>
|
<path d="M20 17v-12c0 -1.121 -.879 -2 -2 -2s-2 .879 -2 2v12l2 2l2 -2z"></path>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 584 B After Width: | Height: | Size: 603 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||||
<path d="M18 6l-12 12"></path>
|
<path d="M18 6l-12 12"></path>
|
||||||
<path d="M6 6l12 12"></path>
|
<path d="M6 6l12 12"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 309 B |
@@ -1,18 +1,22 @@
|
|||||||
<% uuid = local_assigns[:uuid] || SecureRandom.uuid %>
|
<% uuid = local_assigns[:uuid] || SecureRandom.uuid %>
|
||||||
<input type="checkbox" id="<%= uuid %>" class="modal-toggle">
|
<% title_id = "#{uuid}-title" %>
|
||||||
<div id="<%= local_assigns[:id] %>" class="modal items-start !animate-none overflow-y-auto">
|
<%= tag.dialog id: uuid, class: 'modal items-start overflow-y-auto', inert: true, 'aria-labelledby': (title_id if local_assigns[:title]) do %>
|
||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
|
||||||
<% if local_assigns[:title] %>
|
<% if local_assigns[:title] %>
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span>
|
<span id="<%= title_id %>">
|
||||||
<%= local_assigns[:title] %>
|
<%= local_assigns[:title] %>
|
||||||
</span>
|
</span>
|
||||||
<label for="<%= uuid %>" class="text-xl">×</label>
|
<form method="dialog">
|
||||||
|
<button class="text-xl cursor-pointer w-6 h-6" aria-label="<%= t('close') %>">×</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div>
|
<div>
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label class="modal-backdrop" for="<%= uuid %>"></label>
|
<form method="dialog" class="modal-backdrop">
|
||||||
</div>
|
<button aria-label="<%= t('close') %>"></button>
|
||||||
|
</form>
|
||||||
|
<% end %>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
@@ -32,5 +32,5 @@
|
|||||||
</toggle-submit>
|
</toggle-submit>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<% I18n.with_locale(@template.account.locale) do %>
|
<% I18n.with_locale(@template.account.locale) do %>
|
||||||
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
|
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
@@ -51,4 +51,4 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || @submitter&.name, email: params[:email] || @submitter&.email, phone: params[:phone] || @submitter&.phone } }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
|
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || @submitter&.name, email: params[:email] || @submitter&.email, phone: params[:phone] || @submitter&.phone } }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<% I18n.with_locale(@template.account.locale) do %>
|
<% I18n.with_locale(@template.account.locale) do %>
|
||||||
<% content_for(:html_description, t('share_link_is_currently_disabled')) %>
|
<% content_for(:html_description, t('share_link_is_currently_disabled')) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
|
<main class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
|
||||||
<div class="text-center w-full space-y-6">
|
<div class="text-center w-full space-y-6">
|
||||||
<%= render 'banner' %>
|
<%= render 'banner' %>
|
||||||
<p class="text-xl font-semibold text-center">
|
<p class="text-xl font-semibold text-center">
|
||||||
@@ -27,5 +27,5 @@
|
|||||||
<%= button_to button_title(title: t('enable_shared_link'), icon: svg_icon('lock_open', class: 'w-6 h-6')), template_share_link_path(@template), params: { template: { shared_link: true }, redir: start_form_path(slug: @template.slug) }, method: :post, class: 'white-button w-full' %>
|
<%= button_to button_title(title: t('enable_shared_link'), icon: svg_icon('lock_open', class: 'w-6 h-6')), template_share_link_path(@template), params: { template: { shared_link: true }, redir: start_form_path(slug: @template.slug) }, method: :post, class: 'white-button w-full' %>
|
||||||
</toggle-submit>
|
</toggle-submit>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</main>
|
||||||
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
|
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<% I18n.with_locale(@template.account.locale) do %>
|
<% I18n.with_locale(@template.account.locale) do %>
|
||||||
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
|
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="text-center w-full space-y-6">
|
<div class="text-center w-full space-y-6">
|
||||||
@@ -63,6 +63,8 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="mt-4">
|
||||||
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
|
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
|
||||||
<%= render 'start_form/policy', account: @template.account %>
|
<%= render 'start_form/policy', account: @template.account %>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
<% font = field.dig('preferences', 'font') %>
|
<% font = field.dig('preferences', 'font') %>
|
||||||
<% font_type = field.dig('preferences', 'font_type') %>
|
<% font_type = field.dig('preferences', 'font_type') %>
|
||||||
<% font_size_px = (field.dig('preferences', 'font_size').presence || Submissions::GenerateResultAttachments::FONT_SIZE).to_i * local_assigns.fetch(:font_scale) { 1000.0 / PdfUtils::US_LETTER_W } %>
|
<% font_size_px = (field.dig('preferences', 'font_size').presence || Submissions::GenerateResultAttachments::FONT_SIZE).to_i * local_assigns.fetch(:font_scale) { 1000.0 / PdfUtils::US_LETTER_W } %>
|
||||||
<field-value dir="auto" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %><%= "background: #{bg_color}; " if bg_color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)">
|
<field-value dir="auto" aria-hidden="true" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %><%= "background: #{bg_color}; " if bg_color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)">
|
||||||
<% if field['type'] == 'signature' %>
|
<% if field['type'] == 'signature' %>
|
||||||
<% is_narrow = area['h'].positive? && ((area['w'] * local_assigns[:page_width]).to_f / (area['h'] * local_assigns[:page_height])) > 4.5 %>
|
<% is_narrow = area['h'].positive? && ((area['w'] * local_assigns[:page_width]).to_f / (area['h'] * local_assigns[:page_height])) > 4.5 %>
|
||||||
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
|
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
|
||||||
<div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
|
<div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
|
||||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>">
|
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" alt="<%= field['name'].presence || field['title'].presence || field['type'] %>">
|
||||||
</div>
|
</div>
|
||||||
<% if (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) && attachment = attachments_index[value] %>
|
<% if (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) && attachment = attachments_index[value] %>
|
||||||
<div class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem] <%= is_narrow ? 'w-1/2' : 'w-full' %>">
|
<div class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem] <%= is_narrow ? 'w-1/2' : 'w-full' %>">
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'].in?(['image', 'initials', 'stamp', 'kba']) && attachments_index[value].image? %>
|
<% elsif field['type'].in?(['image', 'initials', 'stamp', 'kba']) && attachments_index[value].image? %>
|
||||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy" alt="<%= field['name'].presence || field['title'].presence || field['type'] %>">
|
||||||
<% elsif field['type'].in?(['file', 'payment', 'image']) %>
|
<% elsif field['type'].in?(['file', 'payment', 'image']) %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
<div class="px-0.5 flex flex-col justify-center">
|
<div class="px-0.5 flex flex-col justify-center">
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% elsif field['type'] == 'checkbox' %>
|
<% elsif field['type'] == 'checkbox' %>
|
||||||
<div class="w-full flex items-center justify-center">
|
<div class="w-full flex items-center justify-center">
|
||||||
|
<span class="sr-only">☑</span>
|
||||||
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'].in?(%w[multiple radio]) && area['option_uuid'] %>
|
<% elsif field['type'].in?(%w[multiple radio]) && area['option_uuid'] %>
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
<% option_name = option['value'].presence || "Option #{field['options'].index(option) + 1}" %>
|
<% option_name = option['value'].presence || "Option #{field['options'].index(option) + 1}" %>
|
||||||
<% if Array.wrap(value).include?(option_name) %>
|
<% if Array.wrap(value).include?(option_name) %>
|
||||||
<div class="w-full flex items-center justify-center">
|
<div class="w-full flex items-center justify-center">
|
||||||
|
<span class="sr-only">☑</span>
|
||||||
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
<% with_submitter_timezone = configs.find { |e| e.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true %>
|
<% with_submitter_timezone = configs.find { |e| e.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true %>
|
||||||
<% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %>
|
<% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %>
|
||||||
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
|
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
|
||||||
<div style="max-width: 1600px" class="mx-auto pl-4">
|
<main style="max-width: 1600px" class="mx-auto pl-4">
|
||||||
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
|
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
|
||||||
<a href="<%= signed_in? && @submission.account_id == current_account&.id && @submission.template ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
|
<a href="<%= signed_in? && @submission.account_id == current_account&.id && @submission.template ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
|
||||||
<span><%= render 'submissions/logo' %></span>
|
<span><%= render 'submissions/logo' %></span>
|
||||||
<span class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><% (@submission.name || @submission.template.name).split(/(_)/).each do |item| %><%= item %><wbr><% end %></span>
|
<h1 class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><% (@submission.name || @submission.template.name).split(/(_)/).each do |item| %><%= item %><wbr><% end %></h1>
|
||||||
</a>
|
</a>
|
||||||
<div class="space-x-3 flex items-center">
|
<div class="space-x-3 flex items-center">
|
||||||
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
|
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<% if @submission.submitters.to_a.any?(&:completed_at?) %>
|
<% if @submission.submitters.to_a.any?(&:completed_at?) %>
|
||||||
<% if is_all_completed || !is_combined_enabled %>
|
<% if is_all_completed || !is_combined_enabled %>
|
||||||
<div class="join relative">
|
<div class="join relative">
|
||||||
<download-button data-src="<%= local_assigns[:is_preview] ? (@sig_submitter ? submit_form_documents_path(@sig_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact_blank) : submissions_preview_download_index_path(@submission.slug, combined: is_combined_enabled.presence)) : submission_download_index_path(@submission, combined: is_combined_enabled.presence) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed && !is_combined_enabled %>">
|
<download-button role="button" tabindex="0" aria-label="<%= t('download') %>" data-src="<%= local_assigns[:is_preview] ? (@sig_submitter ? submit_form_documents_path(@sig_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact_blank) : submissions_preview_download_index_path(@submission.slug, combined: is_combined_enabled.presence)) : submission_download_index_path(@submission, combined: is_combined_enabled.presence) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed && !is_combined_enabled %>">
|
||||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||||
<%= svg_icon('download', class: 'w-6 h-6') %>
|
<%= svg_icon('download', class: 'w-6 h-6') %>
|
||||||
<span class="hidden md:inline"><%= t('download') %></span>
|
<span class="hidden md:inline"><%= t('download') %></span>
|
||||||
@@ -45,14 +45,14 @@
|
|||||||
</download-button>
|
</download-button>
|
||||||
<% if is_all_completed && !is_combined_enabled %>
|
<% if is_all_completed && !is_combined_enabled %>
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<label tabindex="0" class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500">
|
<label tabindex="0" aria-label="<%= t('download') %>" class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500">
|
||||||
<span class="text-sm align-text-top">
|
<span class="text-sm align-text-top">
|
||||||
<%= svg_icon('chevron_down', class: 'w-6 h-6 flex-shrink-0 stroke-2') %>
|
<%= svg_icon('chevron_down', class: 'w-6 h-6 flex-shrink-0 stroke-2') %>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box text-right">
|
<ul class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box text-right">
|
||||||
<li>
|
<li>
|
||||||
<download-button data-src="<%= local_assigns[:is_preview] ? (@sig_submitter ? submit_form_documents_path(@sig_submitter.slug, { sig: params[:sig], combined: true }.compact) : submissions_preview_download_index_path(@submission.slug, combined: true)) : submission_download_index_path(@submission, combined: true) %>" class="flex items-center">
|
<download-button role="button" tabindex="0" data-src="<%= local_assigns[:is_preview] ? (@sig_submitter ? submit_form_documents_path(@sig_submitter.slug, { sig: params[:sig], combined: true }.compact) : submissions_preview_download_index_path(@submission.slug, combined: true)) : submission_download_index_path(@submission, combined: true) %>" class="flex items-center">
|
||||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||||
<%= svg_icon('download', class: 'w-6 h-6 flex-shrink-0') %>
|
<%= svg_icon('download', class: 'w-6 h-6 flex-shrink-0') %>
|
||||||
<span class="whitespace-nowrap"><%= t('download_combined_pdf') %></span>
|
<span class="whitespace-nowrap"><%= t('download_combined_pdf') %></span>
|
||||||
@@ -74,14 +74,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex md:max-h-[calc(100vh-60px)]">
|
<div class="flex md:max-h-[calc(100vh-60px)]">
|
||||||
<div class="overflow-y-auto overflow-x-hidden hidden lg:block w-52 flex-none pr-3 mt-0.5 pt-0.5">
|
<div class="overflow-y-auto overflow-x-hidden hidden lg:block w-52 flex-none pr-3 mt-0.5 pt-0.5" tabindex="0">
|
||||||
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
||||||
<% schema = Submissions.filtered_conditions_schema(@submission, values:) %>
|
<% schema = Submissions.filtered_conditions_schema(@submission, values:) %>
|
||||||
<% schema.each do |item| %>
|
<% schema.each do |item| %>
|
||||||
<% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
<% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
||||||
<% if document.preview_images.first %>
|
<% if document.preview_images.first %>
|
||||||
<scroll-to data-selector-id="page-<%= document.uuid %>-0" class="block cursor-pointer">
|
<scroll-to data-selector-id="page-<%= document.uuid %>-0" class="block cursor-pointer">
|
||||||
<img src="<%= (document.preview_images.find { |e| e.filename.base.to_i.zero? } || document.preview_images.first).url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
|
<img src="<%= (document.preview_images.find { |e| e.filename.base.to_i.zero? } || document.preview_images.first).url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy" alt="<%= item['name'].presence || document.filename.base %>">
|
||||||
<div class="pb-2 pt-1.5 text-center" dir="auto">
|
<div class="pb-2 pt-1.5 text-center" dir="auto">
|
||||||
<%= item['name'].presence || document.filename.base %>
|
<%= item['name'].presence || document.filename.base %>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div id="document_view" class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
<div id="document_view" class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5" tabindex="0">
|
||||||
<div class="pr-3.5 pl-0.5">
|
<div class="pr-3.5 pl-0.5">
|
||||||
<% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %>
|
<% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %>
|
||||||
<% submitters_index = @submission.submitters.index_by(&:uuid) %>
|
<% submitters_index = @submission.submitters.index_by(&:uuid) %>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_key, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_key, "#{index}.jpg")) %>
|
||||||
<page-container id="<%= "page-#{document.uuid}-#{index}" %>" class="block before:border before:absolute before:top-0 before:bottom-0 before:left-0 before:right-0 before:rounded relative mb-4" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
|
<page-container id="<%= "page-#{document.uuid}-#{index}" %>" class="block before:border before:absolute before:top-0 before:bottom-0 before:left-0 before:right-0 before:rounded relative mb-4" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
|
||||||
<img loading="lazy" src="<%= page.url %>" width="<%= width %>" class="rounded" height="<%= height %>">
|
<img loading="lazy" src="<%= page.url %>" width="<%= width %>" class="rounded" height="<%= height %>" alt="<%= "#{item['name']} - #{t('page')} #{index + 1}" %>">
|
||||||
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
<% document_annots_index[index]&.each do |annot| %>
|
<% document_annots_index[index]&.each do |annot| %>
|
||||||
<%= render 'submissions/annotation', annot: %>
|
<%= render 'submissions/annotation', annot: %>
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="parties_view" class="hidden md:block relative w-full md:w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
|
<div id="parties_view" class="hidden md:block relative w-full md:w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space" tabindex="0">
|
||||||
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600 bg-pink-500 bg-cyan-500 bg-orange-500 bg-lime-500 bg-indigo-500] %>
|
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600 bg-pink-500 bg-cyan-500 bg-orange-500 bg-lime-500 bg-indigo-500] %>
|
||||||
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
|
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
|
||||||
<% submitter_field_counters = Hash.new { 0 } %>
|
<% submitter_field_counters = Hash.new { 0 } %>
|
||||||
@@ -260,10 +260,10 @@
|
|||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<% if field['type'].in?(%w[signature initials]) %>
|
<% if field['type'].in?(%w[signature initials]) %>
|
||||||
<div class="w-full bg-base-300 py-1">
|
<div class="w-full bg-base-300 py-1">
|
||||||
<img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy" alt="<%= field['name'] || field['title'] || field['type'] %>">
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'].in?(['image', 'stamp', 'kba']) && attachments_index[value].image? %>
|
<% elsif field['type'].in?(['image', 'stamp', 'kba']) && attachments_index[value].image? %>
|
||||||
<img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy" alt="<%= field['name'] || field['title'] || field['type'] %>">
|
||||||
<% elsif field['type'].in?(['file', 'payment', 'image']) %>
|
<% elsif field['type'].in?(['file', 'payment', 'image']) %>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
<% Array.wrap(value).each do |val| %>
|
<% Array.wrap(value).each do |val| %>
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</toggle-visible>
|
</toggle-visible>
|
||||||
</div>
|
</main>
|
||||||
<% unless request.headers['HTTP_X_TURBO'] %>
|
<% unless request.headers['HTTP_X_TURBO'] %>
|
||||||
<%= render 'scripts/autosize_field' %>
|
<%= render 'scripts/autosize_field' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<%= form_for '', url: submit_form_decline_index_path(submitter.slug), method: :post do |f| %>
|
<%= form_for '', url: submit_form_decline_index_path(submitter.slug), method: :post do |f| %>
|
||||||
<div class="mt-4 text-center">
|
<div class="form-control mt-4">
|
||||||
<%= t(:notify_the_sender_with_the_reason_you_declined) %>
|
<label class="label justify-center" for="reason"><%= t(:notify_the_sender_with_the_reason_you_declined) %></label>
|
||||||
</div>
|
|
||||||
<div class="form-control mt-2">
|
|
||||||
<%= f.text_area :reason, required: true, class: 'base-input w-full py-2 h-40', dir: 'auto', placeholder: t('provide_a_reason'), style: 'height: 200px', rows: '10' %>
|
<%= f.text_area :reason, required: true, class: 'base-input w-full py-2 h-40', dir: 'auto', placeholder: t('provide_a_reason'), style: 'height: 200px', rows: '10' %>
|
||||||
</div>
|
</div>
|
||||||
<toggle-submit dir="auto" class="form-control mt-4">
|
<toggle-submit dir="auto" class="form-control mt-4">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @submitter.completed_at > 30.minutes.ago || (current_user && current_user.account.submitters.exists?(id: @submitter.id)) %>
|
<% if @submitter.completed_at > 30.minutes.ago || (current_user && current_user.account.submitters.exists?(id: @submitter.id)) %>
|
||||||
<download-button data-src="<%= submit_form_documents_path(@submitter.slug) %>" class="base-button w-full">
|
<download-button role="button" tabindex="0" aria-label="<%= t('download_documents') %>" data-src="<%= submit_form_documents_path(@submitter.slug) %>" class="base-button w-full">
|
||||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||||
<%= svg_icon('download', class: 'w-6 h-6') %>
|
<%= svg_icon('download', class: 'w-6 h-6') %>
|
||||||
<span><%= t('download_documents') %></span>
|
<span><%= t('download_documents') %></span>
|
||||||
@@ -50,5 +50,5 @@
|
|||||||
</toggle-submit>
|
</toggle-submit>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<% I18n.with_locale(@submitter.account.locale) do %>
|
<% I18n.with_locale(@submitter.account.locale) do %>
|
||||||
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %>
|
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<label for="resend_code" id="resend_label" class="link"><%= t(:re_send_email) %></label>
|
<button type="submit" form="resend_code_form" id="resend_label" class="link"><%= t(:re_send_email) %></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<%= f.button button_title(title: t('submit')), class: 'base-button' %>
|
<%= f.button button_title(title: t('submit')), class: 'base-button' %>
|
||||||
</toggle-submit>
|
</toggle-submit>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= button_to t(:re_send_email), submit_form_email_2fa_path, params: { submitter_slug: @submitter.slug, resend: true }, method: :put, id: 'resend_code', class: 'hidden' %>
|
<%= button_to t(:re_send_email), submit_form_email_2fa_path, params: { submitter_slug: @submitter.slug, resend: true }, method: :put, form: { id: 'resend_code_form', class: 'hidden' } %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<% if params[:t] %>
|
<% if params[:t] %>
|
||||||
<fetch-form data-onload="true">
|
<fetch-form data-onload="true">
|
||||||
@@ -69,4 +69,4 @@
|
|||||||
<div><%= t('please_contact_the_requester_to_specify_your_email_for_two_factor_authentication') %></div>
|
<div><%= t('please_contact_the_requester_to_specify_your_email_for_two_factor_authentication') %></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
|
|||||||
@@ -8,28 +8,33 @@
|
|||||||
<% page_blob_struct = Struct.new(:url, :metadata) %>
|
<% page_blob_struct = Struct.new(:url, :metadata) %>
|
||||||
<% schema = Submissions.filtered_conditions_schema(@submitter.submission, values:, include_submitter_uuid: @submitter.uuid) %>
|
<% schema = Submissions.filtered_conditions_schema(@submitter.submission, values:, include_submitter_uuid: @submitter.uuid) %>
|
||||||
<% font_scale = 1000.0 / PdfUtils::US_LETTER_W %>
|
<% font_scale = 1000.0 / PdfUtils::US_LETTER_W %>
|
||||||
<% decline_modal_checkbox_uuid = nil %>
|
<% decline_modal_id = nil %>
|
||||||
<% delegate_modal_checkbox_uuid = nil %>
|
<% delegate_modal_id = nil %>
|
||||||
<div style="max-height: -webkit-fill-available;">
|
<div style="max-height: -webkit-fill-available;">
|
||||||
<div id="scrollbox">
|
<main id="scrollbox">
|
||||||
|
<div id="sr_only_content"></div>
|
||||||
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
||||||
<%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 transition-transform duration-300 %>
|
<%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 transition-transform duration-300 %>
|
||||||
<%= local_assigns[:banner_html] || capture do %>
|
<%= local_assigns[:banner_html] || capture do %>
|
||||||
<%= render('submit_form/banner') %>
|
<%= render('submit_form/banner') %>
|
||||||
<div id="signing_form_header" class="sticky min-[1230px]:static top-0 z-50 bg-base-100 py-2 px-2 flex items-center md:-mx-[8px]" style="margin-bottom: -16px">
|
<header id="signing_form_header" class="sticky min-[1230px]:static top-0 z-50 bg-base-100 py-2 px-2 flex items-center md:-mx-[8px]" style="margin-bottom: -16px">
|
||||||
<div class="text-xl md:text-2xl font-medium focus:text-clip" style="width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
<h1 class="text-xl md:text-2xl font-medium focus:text-clip" style="width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||||
<%= @submitter.submission.name || @submitter.submission.template.name %>
|
<%= @submitter.submission.name || @submitter.submission.template.name %>
|
||||||
</div>
|
</h1>
|
||||||
<div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0">
|
<div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0">
|
||||||
<% if @form_configs[:with_delegate] %>
|
<% if @form_configs[:with_delegate] %>
|
||||||
<label id="delegate_button" for="<%= delegate_modal_checkbox_uuid = SecureRandom.uuid %>" class="btn btn-sm !px-5"><%= t(:delegate) %></label>
|
<modal-button data-target="<%= delegate_modal_id = SecureRandom.uuid %>">
|
||||||
|
<button id="delegate_button" type="button" class="btn btn-sm !px-5"><%= t(:delegate) %></button>
|
||||||
|
</modal-button>
|
||||||
<% if @form_configs[:with_decline] %>
|
<% if @form_configs[:with_decline] %>
|
||||||
<label id="decline_button" for="<%= decline_modal_checkbox_uuid = SecureRandom.uuid %>" class="btn btn-sm px-2" title="<%= t(:decline) %>">
|
<modal-button data-target="<%= decline_modal_id = SecureRandom.uuid %>">
|
||||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
<button id="decline_button" type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>">
|
||||||
</label>
|
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||||
|
</button>
|
||||||
|
</modal-button>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @form_configs[:with_partial_download] %>
|
<% if @form_configs[:with_partial_download] %>
|
||||||
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2" title="<%= t('download') %>">
|
<download-button role="button" tabindex="0" data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2" title="<%= t('download') %>" aria-label="<%= t('download') %>">
|
||||||
<span data-target="download-button.defaultButton">
|
<span data-target="download-button.defaultButton">
|
||||||
<%= svg_icon('download', class: 'w-5 h-5') %>
|
<%= svg_icon('download', class: 'w-5 h-5') %>
|
||||||
</span>
|
</span>
|
||||||
@@ -40,10 +45,12 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<% if @form_configs[:with_decline] %>
|
<% if @form_configs[:with_decline] %>
|
||||||
<label id="decline_button" for="<%= decline_modal_checkbox_uuid = SecureRandom.uuid %>" class="btn btn-sm !px-5"><%= t(:decline) %></label>
|
<modal-button data-target="<%= decline_modal_id = SecureRandom.uuid %>">
|
||||||
|
<button id="decline_button" type="button" class="btn btn-sm !px-5"><%= t(:decline) %></button>
|
||||||
|
</modal-button>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @form_configs[:with_partial_download] %>
|
<% if @form_configs[:with_partial_download] %>
|
||||||
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4">
|
<download-button role="button" tabindex="0" data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4" aria-label="<%= t('download') %>">
|
||||||
<span class="flex items-center justify-center" data-target="download-button.defaultButton">
|
<span class="flex items-center justify-center" data-target="download-button.defaultButton">
|
||||||
<%= svg_icon('download', class: 'w-6 h-6 inline md:hidden') %>
|
<%= svg_icon('download', class: 'w-6 h-6 inline md:hidden') %>
|
||||||
<span class="hidden md:inline"><%= t('download') %></span>
|
<span class="hidden md:inline"><%= t('download') %></span>
|
||||||
@@ -56,23 +63,27 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
<scroll-buttons class="fixed right-5 top-2 hidden md:flex gap-1 z-50 ease-in-out opacity-0 -translate-y-10">
|
<scroll-buttons inert class="fixed right-5 top-2 hidden md:flex gap-1 z-50 ease-in-out opacity-0 -translate-y-10">
|
||||||
<% if @form_configs[:with_delegate] %>
|
<% if @form_configs[:with_delegate] %>
|
||||||
<label id="delegate_button" for="<%= delegate_modal_checkbox_uuid %>" class="btn btn-sm px-0">
|
<modal-button data-target="<%= delegate_modal_id %>">
|
||||||
<span class="min-[1366px]:inline hidden px-3">
|
<button id="delegate_button_mobile" type="button" class="btn btn-sm px-0" aria-label="<%= t(:delegate) %>">
|
||||||
<%= t(:delegate) %>
|
<span class="min-[1366px]:inline hidden px-3">
|
||||||
</span>
|
<%= t(:delegate) %>
|
||||||
<span class="inline min-[1366px]:hidden px-2">
|
</span>
|
||||||
<%= svg_icon('user_share', class: 'w-5 h-5') %>
|
<span class="inline min-[1366px]:hidden px-2">
|
||||||
</span>
|
<%= svg_icon('user_share', class: 'w-5 h-5') %>
|
||||||
</label>
|
</span>
|
||||||
|
</button>
|
||||||
|
</modal-button>
|
||||||
<% if @form_configs[:with_decline] %>
|
<% if @form_configs[:with_decline] %>
|
||||||
<label id="decline_button" for="<%= decline_modal_checkbox_uuid %>" class="btn btn-sm px-2">
|
<modal-button data-target="<%= decline_modal_id %>">
|
||||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
<button id="decline_button_mobile" type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>">
|
||||||
</label>
|
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||||
|
</button>
|
||||||
|
</modal-button>
|
||||||
<% end %>
|
<% end %>
|
||||||
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2">
|
<download-button role="button" tabindex="0" data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2" aria-label="<%= t('download') %>">
|
||||||
<span data-target="download-button.defaultButton">
|
<span data-target="download-button.defaultButton">
|
||||||
<%= svg_icon('download', class: 'w-5 h-5') %>
|
<%= svg_icon('download', class: 'w-5 h-5') %>
|
||||||
</span>
|
</span>
|
||||||
@@ -82,16 +93,18 @@
|
|||||||
</download-button>
|
</download-button>
|
||||||
<% else %>
|
<% else %>
|
||||||
<% if @form_configs[:with_decline] %>
|
<% if @form_configs[:with_decline] %>
|
||||||
<label id="decline_button" for="<%= decline_modal_checkbox_uuid %>" class="btn btn-sm px-0">
|
<modal-button data-target="<%= decline_modal_id %>">
|
||||||
<span class="min-[1366px]:inline hidden px-3">
|
<button id="decline_button_mobile" type="button" class="btn btn-sm px-0" aria-label="<%= t(:decline) %>">
|
||||||
<%= t(:decline) %>
|
<span class="min-[1366px]:inline hidden px-3">
|
||||||
</span>
|
<%= t(:decline) %>
|
||||||
<span class="inline min-[1366px]:hidden px-2">
|
</span>
|
||||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
<span class="inline min-[1366px]:hidden px-2">
|
||||||
</span>
|
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||||
</label>
|
</span>
|
||||||
|
</button>
|
||||||
|
</modal-button>
|
||||||
<% end %>
|
<% end %>
|
||||||
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2">
|
<download-button role="button" tabindex="0" data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2" aria-label="<%= t('download') %>">
|
||||||
<span data-target="download-button.defaultButton">
|
<span data-target="download-button.defaultButton">
|
||||||
<%= svg_icon('download', class: 'w-5 h-5') %>
|
<%= svg_icon('download', class: 'w-5 h-5') %>
|
||||||
</span>
|
</span>
|
||||||
@@ -105,13 +118,14 @@
|
|||||||
<% schema.each do |item| %>
|
<% schema.each do |item| %>
|
||||||
<% document = @submitter.submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %>
|
<% document = @submitter.submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %>
|
||||||
<div id="document-<%= document.uuid %>">
|
<div id="document-<%= document.uuid %>">
|
||||||
|
<div class="sr_only_content"></div>
|
||||||
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
||||||
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
||||||
<% lazyload_metadata = document.preview_images.last&.metadata || Templates::ProcessDocument::US_LETTER_SIZE %>
|
<% lazyload_metadata = document.preview_images.last&.metadata || Templates::ProcessDocument::US_LETTER_SIZE %>
|
||||||
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_key, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_key, "#{index}.jpg")) %>
|
||||||
<page-container class="block relative my-4 shadow-md" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
|
<page-container class="block relative my-4 shadow-md" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
|
||||||
<img loading="lazy" src="<%= page.url %>" width="<%= width %>" height="<%= height %>">
|
<img loading="lazy" src="<%= page.url %>" width="<%= width %>" height="<%= height %>" alt="<%= "#{item['name']} - #{t('page')} #{index + 1}" %>">
|
||||||
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
<% if annots = document_annots_index[index] %>
|
<% if annots = document_annots_index[index] %>
|
||||||
<%= render 'submit_form/annotations', annots: %>
|
<%= render 'submit_form/annotations', annots: %>
|
||||||
@@ -140,7 +154,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<div class="fixed bottom-0 w-full h-0 z-50">
|
<div class="fixed bottom-0 w-full h-0 z-50">
|
||||||
<div class="mx-auto" style="max-width: 1000px">
|
<div class="mx-auto" style="max-width: 1000px">
|
||||||
@@ -150,12 +164,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% if @form_configs[:with_decline] %>
|
<% if @form_configs[:with_decline] %>
|
||||||
<%= render 'shared/html_modal', title: t(:decline), uuid: decline_modal_checkbox_uuid do %>
|
<%= render 'shared/html_modal', title: t(:decline), uuid: decline_modal_id do %>
|
||||||
<%= render 'submit_form/decline_form', submitter: @submitter %>
|
<%= render 'submit_form/decline_form', submitter: @submitter %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @form_configs[:with_delegate] %>
|
<% if @form_configs[:with_delegate] %>
|
||||||
<%= render 'shared/html_modal', title: t(:delegate), uuid: delegate_modal_checkbox_uuid do %>
|
<%= render 'shared/html_modal', title: t(:delegate), uuid: delegate_modal_id do %>
|
||||||
<%= render 'submit_form/delegate_form', submitter: @submitter %>
|
<%= render 'submit_form/delegate_form', submitter: @submitter %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -23,7 +23,12 @@
|
|||||||
</label>
|
</label>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="flex gap-2 mt-3">
|
<div class="flex gap-2 mt-3">
|
||||||
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug, host: form_link_host) %>" class="base-input w-full" autocomplete="off" readonly>
|
<div class="relative flex-grow">
|
||||||
|
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug, host: form_link_host) %>" class="base-input w-full pr-10" autocomplete="off" readonly>
|
||||||
|
<a href="<%= template_share_link_qr_path(@template) %>" target="_blank" rel="noopener" class="absolute top-1/2 -translate-y-1/2 right-2 flex items-center justify-center tooltip tooltip-left text-base-content/70 hover:text-base-content bg-white rounded px-1 py-0.5" data-tip="<%= t('qr_code') %>" aria-label="<%= t('qr_code') %>">
|
||||||
|
<%= svg_icon('qrcode', class: 'w-6 h-6') %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<check-on-click data-element-id="template_shared_link">
|
<check-on-click data-element-id="template_shared_link">
|
||||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||||
</check-on-click>
|
</check-on-click>
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<%= t('powered_by') %>
|
||||||
|
<a href="<%= Docuseal::PRODUCT_URL %>" target="_blank" rel="noopener"><%= Docuseal.product_name %></a>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<%= render 'shared/logo' %>
|
||||||
|
<span><%= Docuseal.product_name %></span>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
|
||||||
|
<p class="text-xl font-semibold text-center">
|
||||||
|
<%= t('share_link_is_currently_disabled') %>
|
||||||
|
</p>
|
||||||
|
<% if can?(:update, @template) %>
|
||||||
|
<toggle-submit class="block">
|
||||||
|
<%= button_to button_title(title: t('enable_shared_link'), icon: svg_icon('lock_open', class: 'w-6 h-6')), template_share_link_path(@template), params: { template: { shared_link: true }, redir: template_share_link_qr_path(@template) }, method: :post, data: { turbo: false }, class: 'base-button w-full' %>
|
||||||
|
</toggle-submit>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<% page_width_css = @page_size == 'Letter' ? 8.5 * 96.0 : 210.0 * 96.0 / 25.4 %>
|
||||||
|
<% page_height_css = @page_size == 'Letter' ? 11.0 * 96.0 : 297.0 * 96.0 / 25.4 %>
|
||||||
|
<% page_width = @page_size == 'Letter' ? '8.5in' : '210mm' %>
|
||||||
|
<% page_cqw = ->(px) { format('%.6fcqw', px / page_width_css * 100.0) } %>
|
||||||
|
<html lang="<%= I18n.locale %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= @template.name %></title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: <%= @page_size %> portrait;
|
||||||
|
margin: 0.5in;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
color: #111;
|
||||||
|
background: #faf7f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-page-wrapper {
|
||||||
|
container-type: size;
|
||||||
|
width: min(100vw, <%= page_width %>);
|
||||||
|
max-width: 100%;
|
||||||
|
aspect-ratio: <%= format('%<width>.6f / %<height>.6f', width: page_width_css, height: page_height_css) %>;
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-page {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: <%= page_cqw.call(72) %>;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: <%= page_cqw.call(10) %>;
|
||||||
|
font-size: <%= page_cqw.call(20) %>;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-logo svg {
|
||||||
|
width: <%= page_cqw.call(32) %>;
|
||||||
|
height: <%= page_cqw.call(32) %>;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-logo img {
|
||||||
|
height: <%= page_cqw.call(50) %>;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-content {
|
||||||
|
align-self: center;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 0;
|
||||||
|
margin-bottom: <%= page_cqw.call(80) %>;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-header {
|
||||||
|
font-size: <%= page_cqw.call(36) %>;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding: 0 <%= page_cqw.call(8) %>;
|
||||||
|
margin-bottom: <%= page_cqw.call(48) %>;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-main svg {
|
||||||
|
display: block;
|
||||||
|
width: <%= page_cqw.call(480) %>;
|
||||||
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
shape-rendering: crispEdges;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-footer {
|
||||||
|
font-size: <%= page_cqw.call(22) %>;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding: 0 <%= page_cqw.call(8) %>;
|
||||||
|
margin-top: <%= page_cqw.call(48) %>;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-branding {
|
||||||
|
align-self: end;
|
||||||
|
text-align: center;
|
||||||
|
font-size: <%= page_cqw.call(11) %>;
|
||||||
|
color: #6b7280;
|
||||||
|
padding-top: <%= page_cqw.call(24) %>;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-branding a {
|
||||||
|
color: #4b5563;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[contenteditable="true"] {
|
||||||
|
outline: 1px dashed #cbd5e1;
|
||||||
|
outline-offset: 6px;
|
||||||
|
cursor: text;
|
||||||
|
transition: outline-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
[contenteditable="true"]:hover {
|
||||||
|
outline-color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[contenteditable="true"]:focus {
|
||||||
|
outline: 1px dashed #291334;
|
||||||
|
outline-offset: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-button {
|
||||||
|
position: fixed;
|
||||||
|
right: 24px;
|
||||||
|
top: 24px;
|
||||||
|
z-index: 100;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-height: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
border: 1px solid #291334;
|
||||||
|
border-radius: 1.9rem;
|
||||||
|
background-color: #291334;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1em;
|
||||||
|
font-family: inherit;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-button:hover {
|
||||||
|
background-color: #1a0c22;
|
||||||
|
border-color: #1a0c22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-button:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-button:focus-visible {
|
||||||
|
outline: 2px solid #291334;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-button svg {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 820px) {
|
||||||
|
.print-button {
|
||||||
|
top: auto;
|
||||||
|
right: 12px;
|
||||||
|
bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
html, body {
|
||||||
|
background: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-page-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
aspect-ratio: auto;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-page {
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 0.25in;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-logo {
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-logo svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-logo img {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-content {
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-header {
|
||||||
|
font-size: 36px;
|
||||||
|
padding: 0 8px;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-main svg {
|
||||||
|
width: 5in;
|
||||||
|
height: 5in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-footer {
|
||||||
|
font-size: 22px;
|
||||||
|
padding: 0 8px;
|
||||||
|
margin-top: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-branding {
|
||||||
|
font-size: 11px;
|
||||||
|
padding-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-button {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[contenteditable="true"],
|
||||||
|
[contenteditable="true"]:focus {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="qr-page-wrapper">
|
||||||
|
<div class="qr-page">
|
||||||
|
<div class="qr-logo">
|
||||||
|
<%= render 'logo' %>
|
||||||
|
</div>
|
||||||
|
<div class="qr-content">
|
||||||
|
<div class="qr-header" contenteditable="true" spellcheck="false"><%= @template.name %></div>
|
||||||
|
<div class="qr-main">
|
||||||
|
<%== @qr_svg_code %>
|
||||||
|
</div>
|
||||||
|
<div class="qr-footer" contenteditable="true" spellcheck="false">
|
||||||
|
<%= t('scan_the_qr_code_above_with_your_phone_camera_to_open_and_sign_this_document') %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="qr-branding">
|
||||||
|
<%= render 'branding' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="qr-print-button" class="print-button">
|
||||||
|
<%= svg_icon('printer') %>
|
||||||
|
<span><%= t('print') %></span>
|
||||||
|
</button>
|
||||||
|
<script nonce="<%= content_security_policy_nonce %>">
|
||||||
|
document.getElementById('qr-print-button').addEventListener('click', function () {
|
||||||
|
window.print();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -8,11 +8,21 @@
|
|||||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= form_for @webhook_url, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
<% if @webhook_url.new_record? %>
|
||||||
<%= f.label :url, 'Webhook URL', class: 'text-sm font-semibold' %>
|
<%= form_for @webhook_url, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
<div class="space-y-2 md:flex-nowrap mt-2">
|
<%= f.label :url, 'Webhook URL', class: 'text-sm font-semibold' %>
|
||||||
<%= f.url_field :url, class: 'base-input w-full', placeholder: 'https://example.com/hook' %>
|
<div class="space-y-2 md:flex-nowrap mt-2">
|
||||||
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full' %>
|
<%= f.url_field :url, class: 'base-input w-full', placeholder: 'https://example.com/hook' %>
|
||||||
</div>
|
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<%= form_for @webhook_url, url: settings_webhook_path(@webhook_url), method: :put, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
|
<%= f.label :url, 'Webhook URL', class: 'text-sm font-semibold' %>
|
||||||
|
<div class="space-y-2 md:flex-nowrap mt-2">
|
||||||
|
<%= f.url_field :url, class: 'base-input w-full', placeholder: 'https://example.com/hook', required: true %>
|
||||||
|
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ en: &en
|
|||||||
download_documents: Download documents
|
download_documents: Download documents
|
||||||
downloading: Downloading
|
downloading: Downloading
|
||||||
download: Download
|
download: Download
|
||||||
|
page: Page
|
||||||
decline: Decline
|
decline: Decline
|
||||||
declined: Declined
|
declined: Declined
|
||||||
delegate: Delegate
|
delegate: Delegate
|
||||||
@@ -367,6 +368,9 @@ en: &en
|
|||||||
sign_out: Sign out
|
sign_out: Sign out
|
||||||
page_number: 'Page %{number}'
|
page_number: 'Page %{number}'
|
||||||
powered_by: Powered by
|
powered_by: Powered by
|
||||||
|
qr_code: QR Code
|
||||||
|
print: Print
|
||||||
|
scan_the_qr_code_above_with_your_phone_camera_to_open_and_sign_this_document: Scan the QR code above with your phone camera to open and sign this document.
|
||||||
count_documents_signed_with_html: '<b>%{count}</b> documents signed with'
|
count_documents_signed_with_html: '<b>%{count}</b> documents signed with'
|
||||||
storage: Storage
|
storage: Storage
|
||||||
notifications: Notifications
|
notifications: Notifications
|
||||||
@@ -1201,6 +1205,7 @@ es: &es
|
|||||||
download_documents: Descargar documentos
|
download_documents: Descargar documentos
|
||||||
downloading: Descargando
|
downloading: Descargando
|
||||||
download: Descargar
|
download: Descargar
|
||||||
|
page: Página
|
||||||
decline: Rechazar
|
decline: Rechazar
|
||||||
delegate: Delegar
|
delegate: Delegar
|
||||||
enter_the_email_address_of_the_person_you_want_to_delegate_to: Ingrese la dirección de correo electrónico de la persona a quien desea delegar
|
enter_the_email_address_of_the_person_you_want_to_delegate_to: Ingrese la dirección de correo electrónico de la persona a quien desea delegar
|
||||||
@@ -1406,6 +1411,9 @@ es: &es
|
|||||||
sign_out: Cerrar sesión
|
sign_out: Cerrar sesión
|
||||||
page_number: 'Página %{number}'
|
page_number: 'Página %{number}'
|
||||||
powered_by: Desarrollado por
|
powered_by: Desarrollado por
|
||||||
|
qr_code: Código QR
|
||||||
|
print: Imprimir
|
||||||
|
scan_the_qr_code_above_with_your_phone_camera_to_open_and_sign_this_document: Escanea el código QR de arriba con la cámara de tu teléfono para abrir y firmar este documento.
|
||||||
count_documents_signed_with_html: '<b>%{count}</b> documentos firmados con'
|
count_documents_signed_with_html: '<b>%{count}</b> documentos firmados con'
|
||||||
storage: Almacenamiento
|
storage: Almacenamiento
|
||||||
notifications: Notificaciones
|
notifications: Notificaciones
|
||||||
@@ -2237,6 +2245,7 @@ it: &it
|
|||||||
download_documents: Scarica documenti
|
download_documents: Scarica documenti
|
||||||
downloading: Scaricamento
|
downloading: Scaricamento
|
||||||
download: Scarica
|
download: Scarica
|
||||||
|
page: Pagina
|
||||||
decline: Rifiuta
|
decline: Rifiuta
|
||||||
delegate: Delega
|
delegate: Delega
|
||||||
enter_the_email_address_of_the_person_you_want_to_delegate_to: Inserisci l'indirizzo email della persona a cui vuoi delegare
|
enter_the_email_address_of_the_person_you_want_to_delegate_to: Inserisci l'indirizzo email della persona a cui vuoi delegare
|
||||||
@@ -2442,6 +2451,9 @@ it: &it
|
|||||||
sign_out: Esci
|
sign_out: Esci
|
||||||
page_number: 'Pagina %{number}'
|
page_number: 'Pagina %{number}'
|
||||||
powered_by: Fornito da
|
powered_by: Fornito da
|
||||||
|
qr_code: Codice QR
|
||||||
|
print: Stampa
|
||||||
|
scan_the_qr_code_above_with_your_phone_camera_to_open_and_sign_this_document: Scansiona il codice QR qui sopra con la fotocamera del tuo telefono per aprire e firmare questo documento.
|
||||||
count_documents_signed_with_html: '<b>%{count}</b> documenti firmati con'
|
count_documents_signed_with_html: '<b>%{count}</b> documenti firmati con'
|
||||||
storage: Archiviazione
|
storage: Archiviazione
|
||||||
notifications: Notifiche
|
notifications: Notifiche
|
||||||
@@ -3274,6 +3286,7 @@ fr: &fr
|
|||||||
download_documents: Télécharger les documents
|
download_documents: Télécharger les documents
|
||||||
downloading: Téléchargement
|
downloading: Téléchargement
|
||||||
download: Télécharger
|
download: Télécharger
|
||||||
|
page: Page
|
||||||
decline: Refuser
|
decline: Refuser
|
||||||
delegate: Déléguer
|
delegate: Déléguer
|
||||||
enter_the_email_address_of_the_person_you_want_to_delegate_to: Saisissez l'adresse e-mail de la personne à qui vous souhaitez déléguer
|
enter_the_email_address_of_the_person_you_want_to_delegate_to: Saisissez l'adresse e-mail de la personne à qui vous souhaitez déléguer
|
||||||
@@ -3479,6 +3492,9 @@ fr: &fr
|
|||||||
sign_out: Se déconnecter
|
sign_out: Se déconnecter
|
||||||
page_number: Page %{number}
|
page_number: Page %{number}
|
||||||
powered_by: Propulsé par
|
powered_by: Propulsé par
|
||||||
|
qr_code: Code QR
|
||||||
|
print: Imprimer
|
||||||
|
scan_the_qr_code_above_with_your_phone_camera_to_open_and_sign_this_document: Scannez le code QR ci-dessus avec l'appareil photo de votre téléphone pour ouvrir et signer ce document.
|
||||||
count_documents_signed_with_html: "<b>%{count}</b> documents signés avec"
|
count_documents_signed_with_html: "<b>%{count}</b> documents signés avec"
|
||||||
storage: Stockage
|
storage: Stockage
|
||||||
notifications: Notifications
|
notifications: Notifications
|
||||||
@@ -4307,6 +4323,7 @@ pt: &pt
|
|||||||
download_documents: Baixar documentos
|
download_documents: Baixar documentos
|
||||||
downloading: Baixando
|
downloading: Baixando
|
||||||
download: Baixar
|
download: Baixar
|
||||||
|
page: Página
|
||||||
decline: Recusar
|
decline: Recusar
|
||||||
delegate: Delegar
|
delegate: Delegar
|
||||||
enter_the_email_address_of_the_person_you_want_to_delegate_to: Insira o endereço de e-mail da pessoa para quem deseja delegar
|
enter_the_email_address_of_the_person_you_want_to_delegate_to: Insira o endereço de e-mail da pessoa para quem deseja delegar
|
||||||
@@ -4512,6 +4529,9 @@ pt: &pt
|
|||||||
sign_out: Sair
|
sign_out: Sair
|
||||||
page_number: 'Página %{number}'
|
page_number: 'Página %{number}'
|
||||||
powered_by: Desenvolvido por
|
powered_by: Desenvolvido por
|
||||||
|
qr_code: Código QR
|
||||||
|
print: Imprimir
|
||||||
|
scan_the_qr_code_above_with_your_phone_camera_to_open_and_sign_this_document: Escaneie o código QR acima com a câmera do seu telefone para abrir e assinar este documento.
|
||||||
count_documents_signed_with_html: '<b>%{count}</b> documentos assinados com'
|
count_documents_signed_with_html: '<b>%{count}</b> documentos assinados com'
|
||||||
storage: Armazenamento
|
storage: Armazenamento
|
||||||
notifications: Notificações
|
notifications: Notificações
|
||||||
@@ -5343,6 +5363,7 @@ de: &de
|
|||||||
download_documents: Dokumente herunterladen
|
download_documents: Dokumente herunterladen
|
||||||
downloading: Wird heruntergeladen
|
downloading: Wird heruntergeladen
|
||||||
download: Download
|
download: Download
|
||||||
|
page: Seite
|
||||||
decline: Ablehnen
|
decline: Ablehnen
|
||||||
delegate: Delegieren
|
delegate: Delegieren
|
||||||
enter_the_email_address_of_the_person_you_want_to_delegate_to: Geben Sie die E-Mail-Adresse der Person ein, an die Sie delegieren möchten
|
enter_the_email_address_of_the_person_you_want_to_delegate_to: Geben Sie die E-Mail-Adresse der Person ein, an die Sie delegieren möchten
|
||||||
@@ -5548,6 +5569,9 @@ de: &de
|
|||||||
sign_out: Abmelden
|
sign_out: Abmelden
|
||||||
page_number: 'Seite %{number}'
|
page_number: 'Seite %{number}'
|
||||||
powered_by: Bereitgestellt von
|
powered_by: Bereitgestellt von
|
||||||
|
qr_code: QR-Code
|
||||||
|
print: Drucken
|
||||||
|
scan_the_qr_code_above_with_your_phone_camera_to_open_and_sign_this_document: Scannen Sie den QR-Code oben mit Ihrer Handykamera, um dieses Dokument zu öffnen und zu unterzeichnen.
|
||||||
count_documents_signed_with_html: '<b>%{count}</b> Dokumente signiert mit'
|
count_documents_signed_with_html: '<b>%{count}</b> Dokumente signiert mit'
|
||||||
storage: Speicher
|
storage: Speicher
|
||||||
notifications: Benachrichtigungen
|
notifications: Benachrichtigungen
|
||||||
@@ -6243,6 +6267,7 @@ pl:
|
|||||||
view: Widok
|
view: Widok
|
||||||
hi_there: Cześć,
|
hi_there: Cześć,
|
||||||
download: Pobierz
|
download: Pobierz
|
||||||
|
page: Strona
|
||||||
decline: Odrzuć
|
decline: Odrzuć
|
||||||
delegate: Deleguj
|
delegate: Deleguj
|
||||||
enter_the_email_address_of_the_person_you_want_to_delegate_to: Wprowadź adres e-mail osoby, do której chcesz delegować
|
enter_the_email_address_of_the_person_you_want_to_delegate_to: Wprowadź adres e-mail osoby, do której chcesz delegować
|
||||||
@@ -6343,6 +6368,7 @@ uk:
|
|||||||
view: Переглянути
|
view: Переглянути
|
||||||
hi_there: Привіт,
|
hi_there: Привіт,
|
||||||
download: Завантажити
|
download: Завантажити
|
||||||
|
page: Сторінка
|
||||||
decline: Відхилити
|
decline: Відхилити
|
||||||
delegate: Делегувати
|
delegate: Делегувати
|
||||||
enter_the_email_address_of_the_person_you_want_to_delegate_to: Введіть адресу електронної пошти особи, якій ви хочете делегувати
|
enter_the_email_address_of_the_person_you_want_to_delegate_to: Введіть адресу електронної пошти особи, якій ви хочете делегувати
|
||||||
@@ -6443,6 +6469,7 @@ cs:
|
|||||||
view: Zobrazit
|
view: Zobrazit
|
||||||
hi_there: Ahoj,
|
hi_there: Ahoj,
|
||||||
download: Stáhnout
|
download: Stáhnout
|
||||||
|
page: Stránka
|
||||||
decline: Odmítnout
|
decline: Odmítnout
|
||||||
delegate: Delegovat
|
delegate: Delegovat
|
||||||
enter_the_email_address_of_the_person_you_want_to_delegate_to: Zadejte e-mailovou adresu osoby, na kterou chcete delegovat
|
enter_the_email_address_of_the_person_you_want_to_delegate_to: Zadejte e-mailovou adresu osoby, na kterou chcete delegovat
|
||||||
@@ -6543,6 +6570,7 @@ he:
|
|||||||
view: תצוגה
|
view: תצוגה
|
||||||
hi_there: שלום,
|
hi_there: שלום,
|
||||||
download: הורד
|
download: הורד
|
||||||
|
page: עמוד
|
||||||
decline: דחייה
|
decline: דחייה
|
||||||
delegate: הואלה
|
delegate: הואלה
|
||||||
enter_the_email_address_of_the_person_you_want_to_delegate_to: הזן את כתובת הדוא"ל של האדם שברצונך להאציל אליו
|
enter_the_email_address_of_the_person_you_want_to_delegate_to: הזן את כתובת הדוא"ל של האדם שברצונך להאציל אליו
|
||||||
@@ -6780,6 +6808,7 @@ nl: &nl
|
|||||||
download_documents: Documenten downloaden
|
download_documents: Documenten downloaden
|
||||||
downloading: Downloaden
|
downloading: Downloaden
|
||||||
download: Downloaden
|
download: Downloaden
|
||||||
|
page: Pagina
|
||||||
decline: Weigeren
|
decline: Weigeren
|
||||||
delegate: Delegeren
|
delegate: Delegeren
|
||||||
enter_the_email_address_of_the_person_you_want_to_delegate_to: Voer het e-mailadres in van de persoon aan wie u wilt delegeren
|
enter_the_email_address_of_the_person_you_want_to_delegate_to: Voer het e-mailadres in van de persoon aan wie u wilt delegeren
|
||||||
@@ -6985,6 +7014,9 @@ nl: &nl
|
|||||||
sign_out: Afmelden
|
sign_out: Afmelden
|
||||||
page_number: Pagina %{number}
|
page_number: Pagina %{number}
|
||||||
powered_by: Aangedreven door
|
powered_by: Aangedreven door
|
||||||
|
qr_code: QR-code
|
||||||
|
print: Afdrukken
|
||||||
|
scan_the_qr_code_above_with_your_phone_camera_to_open_and_sign_this_document: Scan de bovenstaande QR-code met je telefooncamera om dit document te openen en te ondertekenen.
|
||||||
count_documents_signed_with_html: "<b>%{count}</b> documenten ondertekend met"
|
count_documents_signed_with_html: "<b>%{count}</b> documenten ondertekend met"
|
||||||
storage: Opslag
|
storage: Opslag
|
||||||
notifications: Meldingen
|
notifications: Meldingen
|
||||||
@@ -7676,6 +7708,7 @@ ar:
|
|||||||
view: عرض
|
view: عرض
|
||||||
hi_there: مرحبا,
|
hi_there: مرحبا,
|
||||||
download: تحميل
|
download: تحميل
|
||||||
|
page: صفحة
|
||||||
decline: رفض
|
decline: رفض
|
||||||
delegate: تفويض
|
delegate: تفويض
|
||||||
enter_the_email_address_of_the_person_you_want_to_delegate_to: أدخل عنوان البريد الإلكتروني للشخص الذي تريد التفويض إليه
|
enter_the_email_address_of_the_person_you_want_to_delegate_to: أدخل عنوان البريد الإلكتروني للشخص الذي تريد التفويض إليه
|
||||||
@@ -7776,6 +7809,7 @@ ko:
|
|||||||
view: 보기
|
view: 보기
|
||||||
hi_there: 안녕하세요,
|
hi_there: 안녕하세요,
|
||||||
download: 다운로드
|
download: 다운로드
|
||||||
|
page: 페이지
|
||||||
decline: 거절
|
decline: 거절
|
||||||
delegate: 위임
|
delegate: 위임
|
||||||
enter_the_email_address_of_the_person_you_want_to_delegate_to: 위임할 사람의 이메일 주소를 입력하세요
|
enter_the_email_address_of_the_person_you_want_to_delegate_to: 위임할 사람의 이메일 주소를 입력하세요
|
||||||
@@ -7876,6 +7910,7 @@ ja:
|
|||||||
view: 表示
|
view: 表示
|
||||||
hi_there: こんにちは
|
hi_there: こんにちは
|
||||||
download: ダウンロード
|
download: ダウンロード
|
||||||
|
page: ページ
|
||||||
decline: 辞退
|
decline: 辞退
|
||||||
delegate: 委任
|
delegate: 委任
|
||||||
enter_the_email_address_of_the_person_you_want_to_delegate_to: 委任したい人のメールアドレスを入力してください
|
enter_the_email_address_of_the_person_you_want_to_delegate_to: 委任したい人のメールアドレスを入力してください
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ Rails.application.routes.draw do
|
|||||||
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
||||||
resource :preferences, only: %i[show create destroy], controller: 'templates_preferences'
|
resource :preferences, only: %i[show create destroy], controller: 'templates_preferences'
|
||||||
resource :share_link, only: %i[show create], controller: 'templates_share_link'
|
resource :share_link, only: %i[show create], controller: 'templates_share_link'
|
||||||
|
resource :share_link_qr, only: %i[show], controller: 'templates_share_link_qr'
|
||||||
resources :recipients, only: %i[create], controller: 'templates_recipients'
|
resources :recipients, only: %i[create], controller: 'templates_recipients'
|
||||||
resources :prefillable_fields, only: %i[create], controller: 'templates_prefillable_fields'
|
resources :prefillable_fields, only: %i[create], controller: 'templates_prefillable_fields'
|
||||||
resources :submissions_export, only: %i[index new]
|
resources :submissions_export, only: %i[index new]
|
||||||
@@ -149,6 +150,7 @@ Rails.application.routes.draw do
|
|||||||
resources :decline, only: %i[create], controller: 'submit_form_decline'
|
resources :decline, only: %i[create], controller: 'submit_form_decline'
|
||||||
resources :delegate, only: %i[create], controller: 'submit_form_delegate'
|
resources :delegate, only: %i[create], controller: 'submit_form_delegate'
|
||||||
resources :invite, only: %i[create], controller: 'submit_form_invite'
|
resources :invite, only: %i[create], controller: 'submit_form_invite'
|
||||||
|
resources :metadata, only: %i[index], controller: 'submit_form_metadata'
|
||||||
resources :debug, only: %i[index], controller: 'submissions_debug' if Rails.env.development?
|
resources :debug, only: %i[index], controller: 'submissions_debug' if Rails.env.development?
|
||||||
get :completed
|
get :completed
|
||||||
get :delegated
|
get :delegated
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateDocumentMetadata < ActiveRecord::Migration[8.1]
|
||||||
|
def change
|
||||||
|
create_table :document_metadata do |t|
|
||||||
|
t.references :account, null: false, foreign_key: true, index: false
|
||||||
|
t.string :blob_checksum, null: false
|
||||||
|
t.text :text_runs, null: false
|
||||||
|
|
||||||
|
t.datetime :created_at, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :document_metadata, %i[account_id blob_checksum], unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
|
ActiveRecord::Schema[8.1].define(version: 2026_04_16_100000) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "btree_gin"
|
enable_extension "btree_gin"
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
@@ -168,6 +168,14 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
|
|||||||
t.index ["submitter_id"], name: "index_document_generation_events_on_submitter_id"
|
t.index ["submitter_id"], name: "index_document_generation_events_on_submitter_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "document_metadata", force: :cascade do |t|
|
||||||
|
t.bigint "account_id", null: false
|
||||||
|
t.string "blob_checksum", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.text "text_runs", null: false
|
||||||
|
t.index ["account_id", "blob_checksum"], name: "index_document_metadata_on_account_id_and_blob_checksum", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "dynamic_document_versions", force: :cascade do |t|
|
create_table "dynamic_document_versions", force: :cascade do |t|
|
||||||
t.text "areas", null: false
|
t.text "areas", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
@@ -553,6 +561,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
|
|||||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "document_generation_events", "submitters"
|
add_foreign_key "document_generation_events", "submitters"
|
||||||
|
add_foreign_key "document_metadata", "accounts"
|
||||||
add_foreign_key "dynamic_document_versions", "dynamic_documents"
|
add_foreign_key "dynamic_document_versions", "dynamic_documents"
|
||||||
add_foreign_key "dynamic_documents", "templates"
|
add_foreign_key "dynamic_documents", "templates"
|
||||||
add_foreign_key "email_events", "accounts"
|
add_foreign_key "email_events", "accounts"
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DocumentMetadatas
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def find_or_create_for_document(document, account_id:)
|
||||||
|
checksum = document.blob.checksum
|
||||||
|
|
||||||
|
metadata = DocumentMetadata.find_by(account_id:, blob_checksum: checksum)
|
||||||
|
metadata ||= DocumentMetadata.create!(account_id:, blob_checksum: checksum, text_runs: build_text_runs(document))
|
||||||
|
|
||||||
|
metadata
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_text_runs(document)
|
||||||
|
number_of_pages = document.metadata.dig('pdf', 'number_of_pages').to_i
|
||||||
|
|
||||||
|
return {} if number_of_pages.zero?
|
||||||
|
|
||||||
|
Pdfium::Document.open_bytes(document.download) do |doc|
|
||||||
|
(0...doc.page_count).each_with_object({}) do |page_index, acc|
|
||||||
|
page = doc.get_page(page_index)
|
||||||
|
|
||||||
|
acc[page_index] = page.text_objects.map do |node|
|
||||||
|
{ text: node.content, x: node.x, y: node.y, w: node.w, h: node.h, font_size: node.font_size }
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
page&.close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,6 +4,7 @@ module Mcp
|
|||||||
module HandleRequest
|
module HandleRequest
|
||||||
TOOLS = [
|
TOOLS = [
|
||||||
Mcp::Tools::SearchTemplates,
|
Mcp::Tools::SearchTemplates,
|
||||||
|
Mcp::Tools::LoadTemplate,
|
||||||
Mcp::Tools::CreateTemplate,
|
Mcp::Tools::CreateTemplate,
|
||||||
Mcp::Tools::SendDocuments,
|
Mcp::Tools::SendDocuments,
|
||||||
Mcp::Tools::SearchDocuments
|
Mcp::Tools::SearchDocuments
|
||||||
|
|||||||
@@ -6,27 +6,22 @@ module Mcp
|
|||||||
SCHEMA = {
|
SCHEMA = {
|
||||||
name: 'create_template',
|
name: 'create_template',
|
||||||
title: 'Create Template',
|
title: 'Create Template',
|
||||||
description: 'Create a template from a PDF. Provide a URL or base64-encoded file content.',
|
description: 'Create a document template. Provide a URL to upload a PDF/DOCX file, or provide only a name ' \
|
||||||
|
'to create an empty template and receive an edit URL where the file can be uploaded via the UI.',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
url: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'URL of the document file to upload'
|
|
||||||
},
|
|
||||||
file: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Base64-encoded file content'
|
|
||||||
},
|
|
||||||
filename: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Filename with extension (required when using file)'
|
|
||||||
},
|
|
||||||
name: {
|
name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Template name (defaults to filename)'
|
description: 'Template name (used as the template name and required when url is not provided)'
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Optional URL of a PDF or DOCX file to upload. If omitted, an empty template is ' \
|
||||||
|
'created and the returned edit_url can be used to upload a file via the UI.'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
required: %w[name]
|
||||||
},
|
},
|
||||||
annotations: {
|
annotations: {
|
||||||
readOnlyHint: false,
|
readOnlyHint: false,
|
||||||
@@ -44,48 +39,44 @@ module Mcp
|
|||||||
|
|
||||||
account = current_user.account
|
account = current_user.account
|
||||||
|
|
||||||
if arguments['file'].present?
|
template = Template.new(
|
||||||
tempfile = Tempfile.new
|
account:,
|
||||||
tempfile.binmode
|
author: current_user,
|
||||||
tempfile.write(Base64.decode64(arguments['file']))
|
folder: account.default_template_folder,
|
||||||
tempfile.rewind
|
name: arguments['name'].to_s.presence || 'New Template',
|
||||||
|
fields: [],
|
||||||
|
schema: []
|
||||||
|
)
|
||||||
|
|
||||||
filename = arguments['filename'] || 'document.pdf'
|
if arguments['url'].present?
|
||||||
elsif arguments['url'].present?
|
|
||||||
tempfile = Tempfile.new
|
tempfile = Tempfile.new
|
||||||
tempfile.binmode
|
tempfile.binmode
|
||||||
tempfile.write(DownloadUtils.call(arguments['url'], validate: true).body)
|
tempfile.write(DownloadUtils.call(arguments['url'], validate: true).body)
|
||||||
tempfile.rewind
|
tempfile.rewind
|
||||||
|
|
||||||
filename = File.basename(URI.decode_www_form_component(arguments['url']))
|
filename = File.basename(URI.decode_www_form_component(arguments['url']))
|
||||||
|
|
||||||
|
file = ActionDispatch::Http::UploadedFile.new(
|
||||||
|
tempfile:,
|
||||||
|
filename:,
|
||||||
|
type: Marcel::MimeType.for(tempfile)
|
||||||
|
)
|
||||||
|
|
||||||
|
template.name = arguments['name'].presence || File.basename(filename, '.*')
|
||||||
|
template.save!
|
||||||
|
|
||||||
|
documents, = Templates::CreateAttachments.call(template, { files: [file] }, extract_fields: true)
|
||||||
|
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||||
|
|
||||||
|
if template.fields.blank?
|
||||||
|
template.fields = Templates::ProcessDocument.normalize_attachment_fields(template, documents)
|
||||||
|
end
|
||||||
|
|
||||||
|
template.update!(schema:)
|
||||||
else
|
else
|
||||||
return { content: [{ type: 'text', text: 'Provide either url or file' }], isError: true }
|
template.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
file = ActionDispatch::Http::UploadedFile.new(
|
|
||||||
tempfile:,
|
|
||||||
filename:,
|
|
||||||
type: Marcel::MimeType.for(tempfile)
|
|
||||||
)
|
|
||||||
|
|
||||||
template = Template.new(
|
|
||||||
account:,
|
|
||||||
author: current_user,
|
|
||||||
folder: account.default_template_folder,
|
|
||||||
name: arguments['name'].presence || File.basename(filename, '.*')
|
|
||||||
)
|
|
||||||
|
|
||||||
template.save!
|
|
||||||
|
|
||||||
documents, = Templates::CreateAttachments.call(template, { files: [file] }, extract_fields: true)
|
|
||||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
|
||||||
|
|
||||||
if template.fields.blank?
|
|
||||||
template.fields = Templates::ProcessDocument.normalize_attachment_fields(template, documents)
|
|
||||||
end
|
|
||||||
|
|
||||||
template.update!(schema:)
|
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(template, 'template.created')
|
WebhookUrls.enqueue_events(template, 'template.created')
|
||||||
|
|
||||||
SearchEntries.enqueue_reindex(template)
|
SearchEntries.enqueue_reindex(template)
|
||||||
@@ -104,7 +95,7 @@ module Mcp
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Mcp
|
||||||
|
module Tools
|
||||||
|
module LoadTemplate
|
||||||
|
SCHEMA = {
|
||||||
|
name: 'load_template',
|
||||||
|
title: 'Load Template',
|
||||||
|
description: 'Load a template with its fields. Each field includes name, type, and the signing role name.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
template_id: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Template identifier'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: %w[template_id]
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: true,
|
||||||
|
destructiveHint: false,
|
||||||
|
idempotentHint: true,
|
||||||
|
openWorldHint: false
|
||||||
|
}
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(arguments, _current_user, current_ability)
|
||||||
|
template = Template.accessible_by(current_ability).find_by(id: arguments['template_id'])
|
||||||
|
|
||||||
|
return { content: [{ type: 'text', text: 'Template not found' }], isError: true } unless template
|
||||||
|
|
||||||
|
current_ability.authorize!(:read, template)
|
||||||
|
|
||||||
|
submitters_index = template.submitters.index_by { |s| s['uuid'] }
|
||||||
|
|
||||||
|
roles = template.submitters.pluck('name')
|
||||||
|
|
||||||
|
fields = template.fields.filter_map do |field|
|
||||||
|
next if field['name'].blank?
|
||||||
|
|
||||||
|
{
|
||||||
|
name: field['name'],
|
||||||
|
type: field['type'],
|
||||||
|
role: submitters_index[field['submitter_uuid']]&.dig('name')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: {
|
||||||
|
id: template.id,
|
||||||
|
name: template.name,
|
||||||
|
roles: roles,
|
||||||
|
fields: fields
|
||||||
|
}.to_json
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -31,6 +31,27 @@ module Mcp
|
|||||||
phone: {
|
phone: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Submitter phone number in E.164 format'
|
description: 'Submitter phone number in E.164 format'
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Signing role name from the template'
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'Prefill field values for this submitter (fields become readonly)',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Field name'
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
description: 'Prefilled value for the field'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: %w[name value]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,9 +80,17 @@ module Mcp
|
|||||||
return { content: [{ type: 'text', text: 'Template has no fields' }], isError: true } if template.fields.blank?
|
return { content: [{ type: 'text', text: 'Template has no fields' }], isError: true } if template.fields.blank?
|
||||||
|
|
||||||
submitters = (arguments['submitters'] || []).map do |s|
|
submitters = (arguments['submitters'] || []).map do |s|
|
||||||
s.slice('email', 'name', 'role', 'phone')
|
attrs = s.slice('email', 'name', 'role', 'phone').compact_blank
|
||||||
.compact_blank
|
|
||||||
.with_indifferent_access
|
fields = Array.wrap(s['fields']).filter_map do |f|
|
||||||
|
next if f['name'].blank?
|
||||||
|
|
||||||
|
{ 'name' => f['name'], 'default_value' => f['value'], 'readonly' => true }
|
||||||
|
end
|
||||||
|
|
||||||
|
attrs['fields'] = fields if fields.present?
|
||||||
|
|
||||||
|
attrs.with_indifferent_access
|
||||||
end
|
end
|
||||||
|
|
||||||
submissions = Submissions.create_from_submitters(
|
submissions = Submissions.create_from_submitters(
|
||||||
|
|||||||
@@ -39,6 +39,16 @@ class Pdfium
|
|||||||
FPDF_RENDER_FORCEHALFTONE = 0x400
|
FPDF_RENDER_FORCEHALFTONE = 0x400
|
||||||
FPDF_PRINTING = 0x800
|
FPDF_PRINTING = 0x800
|
||||||
|
|
||||||
|
TextObject = Struct.new(:content, :x, :y, :w, :h, :font_size) do
|
||||||
|
def endx
|
||||||
|
@endx ||= x + w
|
||||||
|
end
|
||||||
|
|
||||||
|
def endy
|
||||||
|
@endy ||= y + h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
TextNode = Struct.new(:content, :x, :y, :w, :h) do
|
TextNode = Struct.new(:content, :x, :y, :w, :h) do
|
||||||
def endx
|
def endx
|
||||||
@endx ||= x + w
|
@endx ||= x + w
|
||||||
@@ -117,6 +127,10 @@ class Pdfium
|
|||||||
attach_function :FPDFPathSegment_GetType, [:FPDF_PATHSEGMENT], :int
|
attach_function :FPDFPathSegment_GetType, [:FPDF_PATHSEGMENT], :int
|
||||||
attach_function :FPDFPathSegment_GetPoint, %i[FPDF_PATHSEGMENT pointer pointer], :int
|
attach_function :FPDFPathSegment_GetPoint, %i[FPDF_PATHSEGMENT pointer pointer], :int
|
||||||
|
|
||||||
|
# Text page object functions (per-run Tj/TJ extraction)
|
||||||
|
attach_function :FPDFTextObj_GetText, %i[FPDF_PAGEOBJECT FPDF_TEXTPAGE pointer ulong], :ulong
|
||||||
|
attach_function :FPDFTextObj_GetFontSize, %i[FPDF_PAGEOBJECT pointer], :int
|
||||||
|
|
||||||
# Page object types
|
# Page object types
|
||||||
FPDF_PAGEOBJ_UNKNOWN = 0
|
FPDF_PAGEOBJ_UNKNOWN = 0
|
||||||
FPDF_PAGEOBJ_TEXT = 1
|
FPDF_PAGEOBJ_TEXT = 1
|
||||||
@@ -515,6 +529,90 @@ class Pdfium
|
|||||||
Pdfium.FPDFText_ClosePage(text_page) if text_page && !text_page.null?
|
Pdfium.FPDFText_ClosePage(text_page) if text_page && !text_page.null?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def text_objects
|
||||||
|
return @text_objects if @text_objects
|
||||||
|
|
||||||
|
ensure_not_closed!
|
||||||
|
|
||||||
|
@text_objects = []
|
||||||
|
|
||||||
|
object_count = Pdfium.FPDFPage_CountObjects(page_ptr)
|
||||||
|
|
||||||
|
return @text_objects if object_count.zero?
|
||||||
|
|
||||||
|
text_page = Pdfium.FPDFText_LoadPage(page_ptr)
|
||||||
|
|
||||||
|
if text_page.null?
|
||||||
|
Pdfium.check_last_error("Failed to load text page #{page_index}")
|
||||||
|
|
||||||
|
raise PdfiumError, "Failed to load text page #{page_index}, pointer is NULL."
|
||||||
|
end
|
||||||
|
|
||||||
|
left_ptr = FFI::MemoryPointer.new(:float)
|
||||||
|
bottom_ptr = FFI::MemoryPointer.new(:float)
|
||||||
|
right_ptr = FFI::MemoryPointer.new(:float)
|
||||||
|
top_ptr = FFI::MemoryPointer.new(:float)
|
||||||
|
font_size_ptr = FFI::MemoryPointer.new(:float)
|
||||||
|
|
||||||
|
object_count.times do |i|
|
||||||
|
page_object = Pdfium.FPDFPage_GetObject(page_ptr, i)
|
||||||
|
|
||||||
|
next if page_object.null?
|
||||||
|
|
||||||
|
next unless Pdfium.FPDFPageObj_GetType(page_object) == Pdfium::FPDF_PAGEOBJ_TEXT
|
||||||
|
|
||||||
|
needed_bytes = Pdfium.FPDFTextObj_GetText(page_object, text_page, FFI::Pointer::NULL, 0)
|
||||||
|
|
||||||
|
next if needed_bytes < 4
|
||||||
|
|
||||||
|
buffer = FFI::MemoryPointer.new(:uint8, needed_bytes)
|
||||||
|
|
||||||
|
written = Pdfium.FPDFTextObj_GetText(page_object, text_page, buffer, needed_bytes)
|
||||||
|
|
||||||
|
next if written < 4
|
||||||
|
|
||||||
|
content = buffer.read_bytes(written - 2).force_encoding('UTF-16LE').encode('UTF-8')
|
||||||
|
|
||||||
|
next if content.empty?
|
||||||
|
|
||||||
|
next if Pdfium.FPDFPageObj_GetBounds(page_object, left_ptr, bottom_ptr, right_ptr, top_ptr).zero?
|
||||||
|
|
||||||
|
obj_left = left_ptr.read_float
|
||||||
|
obj_bottom = bottom_ptr.read_float
|
||||||
|
obj_right = right_ptr.read_float
|
||||||
|
obj_top = top_ptr.read_float
|
||||||
|
|
||||||
|
obj_width = obj_right - obj_left
|
||||||
|
obj_height = obj_top - obj_bottom
|
||||||
|
|
||||||
|
next if obj_width <= 0 || obj_height <= 0
|
||||||
|
|
||||||
|
font_size =
|
||||||
|
if Pdfium.FPDFTextObj_GetFontSize(page_object, font_size_ptr) == 0
|
||||||
|
obj_height
|
||||||
|
else
|
||||||
|
font_size_ptr.read_float
|
||||||
|
end
|
||||||
|
|
||||||
|
font_size = 8 if font_size == 1
|
||||||
|
|
||||||
|
norm_x = obj_left / width
|
||||||
|
norm_y = (height - obj_top) / height
|
||||||
|
norm_w = obj_width / width
|
||||||
|
norm_h = obj_height / height
|
||||||
|
|
||||||
|
@text_objects << TextObject.new(content, norm_x, norm_y, norm_w, norm_h, font_size)
|
||||||
|
end
|
||||||
|
|
||||||
|
y_threshold = 4.0 / width
|
||||||
|
|
||||||
|
@text_objects = @text_objects.sort do |a, b|
|
||||||
|
(a.endy - b.endy).abs < y_threshold ? a.x <=> b.x : a.endy <=> b.endy
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
Pdfium.FPDFText_ClosePage(text_page) if text_page && !text_page.null?
|
||||||
|
end
|
||||||
|
|
||||||
def line_nodes
|
def line_nodes
|
||||||
return @line_nodes if @line_nodes
|
return @line_nodes if @line_nodes
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ module Submissions
|
|||||||
template_submitter = template_submitters.find { |e| e['uuid'] == uuid }
|
template_submitter = template_submitters.find { |e| e['uuid'] == uuid }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
raise BaseError, 'Invalid submitter params' unless template_submitter
|
||||||
|
|
||||||
template_submitter = template_submitter.except('optional_invite_by_uuid', 'invite_by_uuid',
|
template_submitter = template_submitter.except('optional_invite_by_uuid', 'invite_by_uuid',
|
||||||
'invite_via_field_uuid')
|
'invite_via_field_uuid')
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,8 @@ module Submissions
|
|||||||
|
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
|
||||||
end
|
end
|
||||||
|
elsif submitter_value == true || submitter_value == false
|
||||||
|
submitter_value.to_s
|
||||||
else
|
else
|
||||||
submitter_value
|
submitter_value
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -107,24 +107,33 @@ module Submitters
|
|||||||
reason_field_uuid = params[:with_reason]
|
reason_field_uuid = params[:with_reason]
|
||||||
signature_field_uuid = values.except(reason_field_uuid).keys.first
|
signature_field_uuid = values.except(reason_field_uuid).keys.first
|
||||||
|
|
||||||
signature_field = submitter.submission.template_fields.find { |e| e['uuid'] == signature_field_uuid }
|
signature_field = submitter.submission.template_fields.find do |e|
|
||||||
|
e['uuid'] == signature_field_uuid && e['submitter_uuid'] == submitter.uuid
|
||||||
|
end
|
||||||
|
|
||||||
signature_field['preferences'] ||= {}
|
reason_field = submitter.submission.template_fields.find do |e|
|
||||||
signature_field['preferences']['reason_field_uuid'] = reason_field_uuid
|
e['uuid'] == reason_field_uuid && e['submitter_uuid'] == submitter.uuid
|
||||||
|
end
|
||||||
|
|
||||||
reason_field = submitter.submission.template_fields.find { |e| e['uuid'] == reason_field_uuid }
|
if reason_field
|
||||||
|
if reason_field.dig('preferences', 'signature_field_uuid') != signature_field['uuid']
|
||||||
unless reason_field
|
raise ValidationError, 'Invalid field'
|
||||||
|
end
|
||||||
|
else
|
||||||
reason_field = { 'type' => 'text',
|
reason_field = { 'type' => 'text',
|
||||||
'uuid' => reason_field_uuid,
|
'uuid' => reason_field_uuid,
|
||||||
'name' => I18n.t(:reason),
|
'name' => I18n.t(:reason),
|
||||||
'readonly' => true,
|
'readonly' => true,
|
||||||
|
'preferences' => { 'signature_field_uuid' => signature_field['uuid'] },
|
||||||
'submitter_uuid' => submitter.uuid }
|
'submitter_uuid' => submitter.uuid }
|
||||||
|
|
||||||
submitter.submission.template_fields.insert(submitter.submission.template_fields.index(signature_field) + 1,
|
submitter.submission.template_fields.insert(submitter.submission.template_fields.index(signature_field) + 1,
|
||||||
reason_field)
|
reason_field)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
signature_field['preferences'] ||= {}
|
||||||
|
signature_field['preferences']['reason_field_uuid'] = reason_field_uuid
|
||||||
|
|
||||||
submitter.submission.save!
|
submitter.submission.save!
|
||||||
|
|
||||||
reason_field
|
reason_field
|
||||||
@@ -454,6 +463,9 @@ module Submitters
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_value!(_value, field, _params, submitter, _request)
|
def validate_value!(_value, field, _params, submitter, _request)
|
||||||
|
raise ValidationError, 'Missing field' unless field
|
||||||
|
raise ValidationError, 'Invalid field' if field['submitter_uuid'] != submitter.uuid
|
||||||
|
|
||||||
if field['readonly'] == true
|
if field['readonly'] == true
|
||||||
Rollbar.warning("Readonly field #{submitter.id}: #{field['uuid']}") if defined?(Rollbar)
|
Rollbar.warning("Readonly field #{submitter.id}: #{field['uuid']}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
|||||||
@@ -114,36 +114,44 @@ module Templates
|
|||||||
head_node = PageNode.new(elem: ''.b, page: 0, attachment_uuid: attachment&.uuid)
|
head_node = PageNode.new(elem: ''.b, page: 0, attachment_uuid: attachment&.uuid)
|
||||||
tail_node = head_node
|
tail_node = head_node
|
||||||
|
|
||||||
page_range = page_number ? [page_number] : (0...doc.page_count)
|
page_indexes = page_number ? [page_number] : (0...doc.page_count).to_a
|
||||||
|
|
||||||
fields = page_range.flat_map do |current_page_number|
|
prep_opts = { aspect_ratio:, padding:, split_page: }
|
||||||
next [] if current_page_number >= doc.page_count
|
infer_opts = { confidence: confidence / 3.0, nms:, nmm:, temperature: }
|
||||||
|
|
||||||
page = doc.get_page(current_page_number)
|
image = prepare_page_image(doc.get_page(page_indexes.first), inference:, padding:)
|
||||||
|
current_args = inference.prepare_input(image, **prep_opts)
|
||||||
|
current_wait = inference.enqueue(**current_args, **infer_opts)
|
||||||
|
|
||||||
size_key = page.width > page.height ? :width : :height
|
all_fields = []
|
||||||
size = padding ? inference.resolution * 1.5 : inference.resolution
|
|
||||||
|
|
||||||
data, width, height = page.render_to_bitmap(size_key => size)
|
page_indexes.each_with_index do |current_page_number, i|
|
||||||
|
next_n = page_indexes[i + 1]
|
||||||
|
|
||||||
image = Vips::Image.new_from_memory(data, width, height, 4, :uchar)
|
next_image = next_n ? prepare_page_image(doc.get_page(next_n), inference:, padding:) : nil
|
||||||
|
next_args = next_image ? inference.prepare_input(next_image, **prep_opts) : nil
|
||||||
|
|
||||||
fields = inference.call(image, confidence: confidence / 3.0, nms:, nmm:, split_page:,
|
outputs = current_wait.call
|
||||||
temperature:, aspect_ratio:, padding:)
|
|
||||||
|
|
||||||
text_fields = extract_text_fields_from_page(page)
|
next_wait = next_args ? inference.enqueue(**next_args, **infer_opts) : nil
|
||||||
line_fields = extract_line_fields_from_page(page)
|
|
||||||
|
|
||||||
fields = sort_fields(fields, y_threshold: 10.0 / image.height)
|
fields = inference.process_outputs(outputs, **current_args, **infer_opts)
|
||||||
|
|
||||||
|
current_page = doc.get_page(current_page_number)
|
||||||
|
|
||||||
|
fields = sort_fields(fields, y_threshold: 10.0 / current_args[:image].height)
|
||||||
|
|
||||||
|
text_fields = extract_text_fields_from_page(current_page)
|
||||||
|
line_fields = extract_line_fields_from_page(current_page)
|
||||||
|
|
||||||
fields = increase_confidence_for_overlapping_fields(fields, text_fields, confidence:)
|
fields = increase_confidence_for_overlapping_fields(fields, text_fields, confidence:)
|
||||||
fields = increase_confidence_for_overlapping_fields(fields, line_fields, confidence:)
|
fields = increase_confidence_for_overlapping_fields(fields, line_fields, confidence:)
|
||||||
|
|
||||||
fields = fields.reject { |f| f.confidence < confidence }
|
fields = fields.reject { |f| f.confidence < confidence }
|
||||||
|
|
||||||
field_nodes, tail_node = build_page_nodes(page, fields, tail_node, attachment_uuid: attachment&.uuid)
|
field_nodes, tail_node = build_page_nodes(current_page, fields, tail_node, attachment_uuid: attachment&.uuid)
|
||||||
|
|
||||||
fields = field_nodes.map do |node|
|
page_fields = field_nodes.map do |node|
|
||||||
field = node.elem
|
field = node.elem
|
||||||
|
|
||||||
type = regexp_type ? type_from_page_node(node) : field.type
|
type = regexp_type ? type_from_page_node(node) : field.type
|
||||||
@@ -162,20 +170,32 @@ module Templates
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
yield [attachment&.uuid, current_page_number, fields] if block_given?
|
yield [attachment&.uuid, current_page_number, page_fields] if block_given?
|
||||||
|
|
||||||
fields
|
all_fields.concat(page_fields)
|
||||||
|
|
||||||
|
current_args = next_args
|
||||||
|
current_wait = next_wait
|
||||||
ensure
|
ensure
|
||||||
page.close
|
current_page&.close
|
||||||
end
|
end
|
||||||
|
|
||||||
print_debug(head_node) if Rails.env.development?
|
print_debug(head_node) if Rails.env.development?
|
||||||
|
|
||||||
[fields, head_node]
|
[all_fields, head_node]
|
||||||
ensure
|
ensure
|
||||||
doc.close
|
doc.close
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def prepare_page_image(page, inference:, padding:)
|
||||||
|
size_key = page.width > page.height ? :width : :height
|
||||||
|
size = padding ? inference.resolution * 1.5 : inference.resolution
|
||||||
|
|
||||||
|
data, width, height = page.render_to_bitmap(size_key => size)
|
||||||
|
|
||||||
|
Vips::Image.new_from_memory(data, width, height, 4, :uchar)
|
||||||
|
end
|
||||||
|
|
||||||
def sort_fields(fields, y_threshold: 0.01)
|
def sort_fields(fields, y_threshold: 0.01)
|
||||||
fields.sort do |a, b|
|
fields.sort do |a, b|
|
||||||
(a.endy - b.endy).abs < y_threshold ? a.x <=> b.x : a.endy <=> b.endy
|
(a.endy - b.endy).abs < y_threshold ? a.x <=> b.x : a.endy <=> b.endy
|
||||||
|
|||||||
@@ -73,6 +73,18 @@ module Templates
|
|||||||
build_fields_from_detections(detections, image)
|
build_fields_from_detections(detections, image)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def prepare_input(image, **opts)
|
||||||
|
{ image:, **opts }
|
||||||
|
end
|
||||||
|
|
||||||
|
def enqueue(image:, **infer_opts)
|
||||||
|
-> { call(image, **infer_opts) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_outputs(outputs, **)
|
||||||
|
outputs
|
||||||
|
end
|
||||||
|
|
||||||
def call_v2(image, offset_x, offset_y, split_page, confidence:, resolution:)
|
def call_v2(image, offset_x, offset_y, split_page, confidence:, resolution:)
|
||||||
if split_page && image.height > image.width
|
if split_page && image.height > image.width
|
||||||
regions = build_split_image_regions(image)
|
regions = build_split_image_regions(image)
|
||||||
|
|||||||
@@ -613,7 +613,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
visit submit_form_path(slug: submitter.slug)
|
visit submit_form_path(slug: submitter.slug)
|
||||||
|
|
||||||
find('#expand_form_button').click
|
find('#expand_form_button').click
|
||||||
click_link 'Type'
|
click_button 'Type'
|
||||||
fill_in 'signature_text_input', with: 'John Doe'
|
fill_in 'signature_text_input', with: 'John Doe'
|
||||||
click_button 'Sign and Complete'
|
click_button 'Sign and Complete'
|
||||||
|
|
||||||
@@ -752,7 +752,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
visit submit_form_path(slug: submitter.slug)
|
visit submit_form_path(slug: submitter.slug)
|
||||||
|
|
||||||
find('#expand_form_button').click
|
find('#expand_form_button').click
|
||||||
click_link 'Draw'
|
click_button 'Draw'
|
||||||
draw_canvas
|
draw_canvas
|
||||||
click_button 'Complete'
|
click_button 'Complete'
|
||||||
|
|
||||||
@@ -1169,7 +1169,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
|
|
||||||
find('#decline_button').click
|
find('#decline_button').click
|
||||||
fill_in 'reason', with: 'I do not agree with the terms'
|
fill_in 'reason', with: 'I do not agree with the terms'
|
||||||
click_button 'Decline'
|
within('dialog[open]') { click_button 'Decline' }
|
||||||
|
|
||||||
expect(page).to have_content('Form has been declined')
|
expect(page).to have_content('Form has been declined')
|
||||||
|
|
||||||
@@ -1193,7 +1193,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
|
|
||||||
find('#delegate_button').click
|
find('#delegate_button').click
|
||||||
fill_in 'email', with: 'delegate@example.com'
|
fill_in 'email', with: 'delegate@example.com'
|
||||||
click_button 'Delegate'
|
within('dialog[open]') { click_button 'Delegate' }
|
||||||
|
|
||||||
expect(page).to have_content('Document has been delegated')
|
expect(page).to have_content('Document has been delegated')
|
||||||
|
|
||||||
|
|||||||