From d35538e2ba5fdad4603c492610122533e9522e75 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 19 Sep 2026 11:20:02 +0300 Subject: [PATCH] adjust native --- app/javascript/application.js | 55 +----------------- app/javascript/elements/native_action.js | 4 +- app/javascript/elements/native_menu.js | 4 +- app/javascript/form.js | 3 +- .../{elements/native_bridge.js => native.js} | 56 ++++++++++++++++++- app/javascript/submission_form/completed.vue | 4 +- app/javascript/submission_form/form.vue | 10 +++- .../submission_form/signature_step.vue | 10 +++- .../template_builder/area_title.vue | 10 ++-- app/javascript/template_builder/builder.vue | 10 ++-- .../template_builder/custom_field.vue | 8 +-- app/javascript/template_builder/dropzone.vue | 9 ++- app/javascript/template_builder/field.vue | 10 ++-- app/javascript/template_builder/fields.vue | 4 +- .../template_builder/native_modals.vue | 14 +++-- app/javascript/template_builder/preview.vue | 4 +- .../submit_form/_submission_form.html.erb | 2 +- 17 files changed, 121 insertions(+), 96 deletions(-) rename app/javascript/{elements/native_bridge.js => native.js} (58%) diff --git a/app/javascript/application.js b/app/javascript/application.js index 7521db3cd..6091c1271 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,5 +1,5 @@ import '@hotwired/turbo-rails' -import './elements/native_bridge' +import './native' import { createApp, reactive } from 'vue' import TemplateBuilder from './template_builder/builder' @@ -81,63 +81,12 @@ document.addEventListener('keyup', (e) => { } }) -if (window.webkit?.messageHandlers?.native) { - const staleSnapshots = new Set() - let isRestoring = false - - window.Turbo.session.view.clearSnapshotCache = function () { - this.snapshotCache.keys.forEach((key) => staleSnapshots.add(key)) - } - - document.addEventListener('turbo:submit-end', (e) => { - if (e.detail.success && !e.detail.formSubmission.isSafe) { - window.webkit.messageHandlers.native.postMessage({ type: 'stale' }) - } - }) - - document.addEventListener('native:pull-to-refresh', (event) => { - const { session } = window.Turbo - const url = new URL(document.baseURI) - - url.searchParams.delete('limit') - - session.history.replace(url, session.history.restorationIdentifier) - session.view.lastRenderedLocation = url - - event.detail.result = new Promise((resolve) => { - document.addEventListener('turbo:render', () => resolve(true), { once: true }) - document.addEventListener('turbo:fetch-request-error', () => resolve(true), { once: true }) - - session.refresh(document.baseURI) - }) - }) - - document.addEventListener('native:refresh', () => { - window.Turbo.session.refresh(document.baseURI) - }) - - document.addEventListener('native:turbo-stream', (e) => { - if (!e.detail.body?.trimStart().startsWith(' { - isRestoring = e.detail.action === 'restore' - }) - - document.addEventListener('turbo:load', () => { - if (staleSnapshots.delete(window.location.href.split('#')[0]) && isRestoring && window.location.pathname !== '/search') { - setTimeout(() => window.Turbo.session.refresh(document.baseURI)) - } - }) -} - document.addEventListener('turbo:before-fetch-response', (event) => { if (event.detail.fetchResponse.header('content-disposition')?.includes('attachment')) { event.preventDefault() } }) + document.addEventListener('turbo:submit-end', async (event) => { const resp = event.detail?.formSubmission?.result?.fetchResponse?.response diff --git a/app/javascript/elements/native_action.js b/app/javascript/elements/native_action.js index bce5163bf..256c9000a 100644 --- a/app/javascript/elements/native_action.js +++ b/app/javascript/elements/native_action.js @@ -1,5 +1,3 @@ -let counter = 0 - export default class extends HTMLElement { connectedCallback () { const bridge = window.webkit?.messageHandlers?.native @@ -10,7 +8,7 @@ export default class extends HTMLElement { if (!target && !this.dataset.native) return - this.nativeId ||= `native-action-${++counter}` + this.nativeId ||= `native-action-${Math.random().toString(32).split('.')[1]}` this.id = this.nativeId if (target) target.dataset.nativeId = this.nativeId diff --git a/app/javascript/elements/native_menu.js b/app/javascript/elements/native_menu.js index e849b284c..34c8c4ea7 100644 --- a/app/javascript/elements/native_menu.js +++ b/app/javascript/elements/native_menu.js @@ -1,5 +1,3 @@ -let counter = 0 - export default class extends HTMLElement { connectedCallback () { if (window.webkit?.messageHandlers?.native) { @@ -28,7 +26,7 @@ export default class extends HTMLElement { const items = [...this.querySelectorAll('native-menu-action')].map((action) => { const target = action.querySelector('a, button, [role="button"]') || action.firstElementChild - target.dataset.nativeId ||= `native-menu-${++counter}` + target.dataset.nativeId ||= `native-menu-${Math.random().toString(32).split('.')[1]}` return { id: target.dataset.nativeId, diff --git a/app/javascript/form.js b/app/javascript/form.js index c06af98e3..83bce1a52 100644 --- a/app/javascript/form.js +++ b/app/javascript/form.js @@ -1,4 +1,4 @@ -import './elements/native_bridge' +import './native' import { createApp, reactive } from 'vue' @@ -45,6 +45,7 @@ safeRegisterElement('submission-form', class extends HTMLElement { withSignatureId: this.dataset.withSignatureId === 'true', requireSigningReason: this.dataset.requireSigningReason === 'true', withConfetti: this.dataset.withConfetti === 'true', + nativePlatform: this.dataset.nativePlatform, withFieldLabels: this.dataset.withFieldLabels !== 'false', withDisclosure: this.dataset.withDisclosure === 'true', reuseSignature: this.dataset.reuseSignature !== 'false', diff --git a/app/javascript/elements/native_bridge.js b/app/javascript/native.js similarity index 58% rename from app/javascript/elements/native_bridge.js rename to app/javascript/native.js index 8f88e0177..3fe988c1d 100644 --- a/app/javascript/elements/native_bridge.js +++ b/app/javascript/native.js @@ -35,7 +35,7 @@ if (handlers?.native && window === window.top) { reportPageFlash() document.addEventListener('click', (event) => { - const link = event.target.closest?.('a[href]') + const link = event.target.closest('a[href]') if (!link) return @@ -52,7 +52,7 @@ if (handlers?.native && window === window.top) { document.addEventListener('click', (event) => { if (document.querySelector('native-modal')) return - const link = event.target.closest?.('a[data-turbo-frame="modal"], a[data-turbo-frame="drawer"]') + const link = event.target.closest('a[data-turbo-frame="modal"], a[data-turbo-frame="drawer"]') if (!link?.href) return @@ -72,3 +72,55 @@ if (handlers?.native && window === window.top) { event.detail.result = rect?.height ? rect.bottom - document.documentElement.getBoundingClientRect().top : null }) } + +if (handlers?.native && window.Turbo) { + const staleSnapshots = new Set() + let isRestoring = false + + window.Turbo.session.view.clearSnapshotCache = function () { + this.snapshotCache.keys.forEach((key) => staleSnapshots.add(key)) + } + + document.addEventListener('turbo:submit-end', (e) => { + if (e.detail.success && !e.detail.formSubmission.isSafe) { + handlers.native.postMessage({ type: 'stale' }) + } + }) + + document.addEventListener('native:pull-to-refresh', (event) => { + const { session } = window.Turbo + const url = new URL(document.baseURI) + + url.searchParams.delete('limit') + + session.history.replace(url, session.history.restorationIdentifier) + session.view.lastRenderedLocation = url + + event.detail.result = new Promise((resolve) => { + document.addEventListener('turbo:render', () => resolve(true), { once: true }) + document.addEventListener('turbo:fetch-request-error', () => resolve(true), { once: true }) + + session.refresh(document.baseURI) + }) + }) + + document.addEventListener('native:refresh', () => { + window.Turbo.session.refresh(document.baseURI) + }) + + document.addEventListener('native:turbo-stream', (e) => { + if (!e.detail.body?.trimStart().startsWith(' { + isRestoring = e.detail.action === 'restore' + }) + + document.addEventListener('turbo:load', () => { + if (staleSnapshots.delete(window.location.href.split('#')[0]) && isRestoring && window.location.pathname !== '/search') { + setTimeout(() => window.Turbo.session.refresh(document.baseURI)) + } + }) +} diff --git a/app/javascript/submission_form/completed.vue b/app/javascript/submission_form/completed.vue index 274303601..809ae3b6e 100644 --- a/app/javascript/submission_form/completed.vue +++ b/app/javascript/submission_form/completed.vue @@ -126,7 +126,7 @@ export default { IconLogin, IconDownload }, - inject: ['baseUrl', 't'], + inject: ['baseUrl', 't', 'nativePlatform'], props: { submitterSlug: { type: String, @@ -196,7 +196,7 @@ export default { }, computed: { isNativeApp () { - return !!window.webkit?.messageHandlers?.download + return !!this.nativePlatform && !!window.webkit?.messageHandlers?.download }, isWebView () { return /webview|wv|ip((?!.*Safari)|(?=.*like Safari))/i.test(window.navigator.userAgent) diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index 6204d0116..e46a9bdf5 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -774,6 +774,7 @@ export default { baseUrl: this.baseUrl, scrollIntoArea: this.scrollIntoArea, scrollIntoField: this.scrollIntoField, + nativePlatform: this.nativePlatform, t: this.t } }, @@ -909,6 +910,11 @@ export default { required: false, default: false }, + nativePlatform: { + type: String, + required: false, + default: '' + }, autoscrollFields: { type: Boolean, required: false, @@ -1767,7 +1773,9 @@ export default { this.isCompleted = true this.isFormVisible = true - window.webkit?.messageHandlers?.native?.postMessage({ type: 'stale' }) + if (this.nativePlatform) { + window.webkit?.messageHandlers?.native?.postMessage({ type: 'stale' }) + } if (resp?.text) { const respData = await resp.text() diff --git a/app/javascript/submission_form/signature_step.vue b/app/javascript/submission_form/signature_step.vue index 01f0271ee..e2406ae13 100644 --- a/app/javascript/submission_form/signature_step.vue +++ b/app/javascript/submission_form/signature_step.vue @@ -376,7 +376,7 @@ export default { IconSignature, IconArrowsDiagonalMinimize2 }, - inject: ['baseUrl', 't'], + inject: ['baseUrl', 't', 'nativePlatform'], props: { field: { type: Object, @@ -580,10 +580,14 @@ export default { }, methods: { lockSwipe () { - window.webkit?.messageHandlers?.native?.postMessage({ type: 'swipe', op: 'lock' }) + if (this.nativePlatform) { + window.webkit?.messageHandlers?.native?.postMessage({ type: 'swipe', op: 'lock' }) + } }, unlockSwipe () { - window.webkit?.messageHandlers?.native?.postMessage({ type: 'swipe', op: 'unlock' }) + if (this.nativePlatform) { + window.webkit?.messageHandlers?.native?.postMessage({ type: 'swipe', op: 'unlock' }) + } }, setCanvasSize () { const canvas = this.$refs.canvas diff --git a/app/javascript/template_builder/area_title.vue b/app/javascript/template_builder/area_title.vue index e8a6f1358..8ca938669 100644 --- a/app/javascript/template_builder/area_title.vue +++ b/app/javascript/template_builder/area_title.vue @@ -272,7 +272,7 @@ export default { FieldSubmitter, IconX }, - inject: ['t', 'isMobile'], + inject: ['t', 'isMobile', 'nativePlatform'], props: { template: { type: Object, @@ -387,28 +387,28 @@ export default { }, methods: { openFormulaModal () { - if (window.webkit?.messageHandlers?.modal) { + if (this.nativePlatform && window.webkit?.messageHandlers?.modal) { window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'formula', detent: 'medium', uuid: this.field.uuid }) } else { this.isShowFormulaModal = true } }, openFontModal () { - if (window.webkit?.messageHandlers?.modal) { + if (this.nativePlatform && window.webkit?.messageHandlers?.modal) { window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'font', detent: 'medium', uuid: this.field.uuid }) } else { this.isShowFontModal = true } }, openConditionsModal () { - if (window.webkit?.messageHandlers?.modal) { + if (this.nativePlatform && window.webkit?.messageHandlers?.modal) { window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'conditions', detent: 'medium', uuid: this.field.uuid }) } else { this.isShowConditionsModal = true } }, openDescriptionModal () { - if (window.webkit?.messageHandlers?.modal) { + if (this.nativePlatform && window.webkit?.messageHandlers?.modal) { window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'description', detent: 'medium', uuid: this.field.uuid }) } else { this.isShowDescriptionModal = true diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 03ddd194d..f1bd7266e 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -1929,7 +1929,7 @@ export default { download () { const download = window.webkit?.messageHandlers?.download - if (download) return download.postMessage({ src: `/templates/${this.template.id}/documents` }) + if (this.nativePlatform && download) return download.postMessage({ src: `/templates/${this.template.id}/documents` }) this.isDownloading = true @@ -2203,7 +2203,7 @@ export default { this.loadRevisionsPromise ||= this.baseFetch(`/templates/${this.template.id}/versions`) }, openRevisionsModal () { - if (window.webkit?.messageHandlers?.modal) { + if (this.nativePlatform && window.webkit?.messageHandlers?.modal) { return window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'revisions', detent: 'medium' }) } @@ -2220,7 +2220,7 @@ export default { }) }, openDocumentsEditor (item) { - if (window.webkit?.messageHandlers?.modal) { + if (this.nativePlatform && window.webkit?.messageHandlers?.modal) { window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'documents', viewport: 'fixed', uuid: item.attachment_uuid }) } else { this.editModalDocumentUuid = item.attachment_uuid @@ -2438,7 +2438,9 @@ export default { ref?.$el.scrollIntoView({ behavior: 'smooth', block: 'start' }) }, postNativeEvent (name, detail, dismiss = false) { - window.webkit?.messageHandlers?.modal?.postMessage({ action: 'dispatch', name, detail: JSON.stringify(detail), dismiss: dismiss ? 'true' : 'false' }) + if (this.nativePlatform) { + window.webkit?.messageHandlers?.modal?.postMessage({ action: 'dispatch', name, detail: JSON.stringify(detail), dismiss: dismiss ? 'true' : 'false' }) + } }, onDrawerScrollTo (item) { if (this.nativeView) { diff --git a/app/javascript/template_builder/custom_field.vue b/app/javascript/template_builder/custom_field.vue index ca9e3d12c..ae3f2fd29 100644 --- a/app/javascript/template_builder/custom_field.vue +++ b/app/javascript/template_builder/custom_field.vue @@ -209,7 +209,7 @@ export default { IconTrashX, FieldType }, - inject: ['backgroundColor', 't', 'isMobile'], + inject: ['backgroundColor', 't', 'isMobile', 'nativePlatform'], props: { field: { type: Object, @@ -261,21 +261,21 @@ export default { }, methods: { openFormulaModal () { - if (window.webkit?.messageHandlers?.modal) { + if (this.nativePlatform && window.webkit?.messageHandlers?.modal) { window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'formula', detent: 'medium', custom_field_uuid: this.field.uuid }) } else { this.isShowFormulaModal = true } }, openFontModal () { - if (window.webkit?.messageHandlers?.modal) { + if (this.nativePlatform && window.webkit?.messageHandlers?.modal) { window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'font', detent: 'medium', custom_field_uuid: this.field.uuid }) } else { this.isShowFontModal = true } }, openDescriptionModal () { - if (window.webkit?.messageHandlers?.modal) { + if (this.nativePlatform && window.webkit?.messageHandlers?.modal) { window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'description', detent: 'medium', custom_field_uuid: this.field.uuid }) } else { this.isShowDescriptionModal = true diff --git a/app/javascript/template_builder/dropzone.vue b/app/javascript/template_builder/dropzone.vue index cbe9230a1..ec9845d79 100644 --- a/app/javascript/template_builder/dropzone.vue +++ b/app/javascript/template_builder/dropzone.vue @@ -38,7 +38,12 @@ v-if="withDescription" class="text-sm" > - {{ t('click_to_upload') }} {{ t('or_drag_and_drop_files') }} + +