From bff3b53fcb4c266fb34d144e7edc71ffe7043cd4 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Mon, 16 Mar 2026 17:21:34 +0700 Subject: [PATCH] refactor: Improve statelessness of iframe bridge restoration and CSS injection, and prevent re-binding of image field events. --- static/src/js/sign_image_upload.js | 175 +++++++++++++++-------------- 1 file changed, 88 insertions(+), 87 deletions(-) diff --git a/static/src/js/sign_image_upload.js b/static/src/js/sign_image_upload.js index 169c210..88a76b7 100755 --- a/static/src/js/sign_image_upload.js +++ b/static/src/js/sign_image_upload.js @@ -4,41 +4,39 @@ import { patch } from "@web/core/utils/patch"; import { PDFIframe } from "@sign/components/sign_request/PDF_iframe"; import { SignablePDFIframe } from "@sign/components/sign_request/signable_PDF_iframe"; -console.log("Sign Image Field: Initializing CSS-forced visibility patches..."); +console.log("Sign Image Field: Initializing stateless CSS-forced visibility patches..."); /** - * Diagnostic Bridge Restoration & Universal Force-Show - * We re-connect the bridge but ALSO forcefully inject CSS to ensure elements are never hidden. + * Stateless Bridge Restoration & Universal Force-Show + * Odoo re-uses and hides iframes when switching between multiple documents in one session. + * We must forcefully reconnect without relying on state markers. */ -const restoreBridgeAndForceShow = (instance, caller) => { +const restoreBridgeAndForceShow = (instance) => { if (!instance || !instance.root || !instance.root.defaultView) return; const iwin = instance.root.defaultView; const iframe = iwin.frameElement; const isDocReady = iwin.document && iwin.document.head; - if (iframe && !iframe.odoo_iframe_instance) { - if (!iframe._odoo_bridge_logged) { - console.log(`[Diagnostic] Restoring bridge on iframe element from ${caller}`); - iframe._odoo_bridge_logged = true; - } + // Connect to Element + if (iframe) { iframe.odoo_iframe_instance = instance; } - if (!iwin.odoo_iframe_instance) { - iwin.odoo_iframe_instance = instance; - } + // Connect to Window + iwin.odoo_iframe_instance = instance; - if (iwin.PDFViewerApplication && !iwin.PDFViewerApplication.odoo_iframe_instance) { + // Connect to Viewer App + if (iwin.PDFViewerApplication) { iwin.PDFViewerApplication.odoo_iframe_instance = instance; } // ULTIMATE FALLBACK: Inject CSS to override Odoo's broken d-none toggler - if (isDocReady && !iwin.document._odoo_force_show_injected) { - console.log(`[Diagnostic] Injecting permanent override CSS from ${caller}`); + // We check for the explicit element ID we inject to ensure we don't spam DOM + if (isDocReady && !iwin.document.getElementById('odoo-force-show-styles')) { const style = iwin.document.createElement('style'); + style.id = 'odoo-force-show-styles'; style.type = 'text/css'; - // Force display block and override any inline or class-based display:none style.innerHTML = ` .o_sign_sign_item.d-none { display: block !important; @@ -48,7 +46,6 @@ const restoreBridgeAndForceShow = (instance, caller) => { } `; iwin.document.head.appendChild(style); - iwin.document._odoo_force_show_injected = true; } }; @@ -82,6 +79,7 @@ const triggerLazyLoad = (el) => { const setupLazyObserver = (el) => { if (!el) return; + // Safety timeout setTimeout(() => { if (el.classList.contains('o_sign_image_loading_container')) { triggerLazyLoad(el); @@ -103,13 +101,12 @@ const setupLazyObserver = (el) => { } }; -const processedItems = new WeakSet(); - -// Patching Strategy: Use refreshSignItemsForPage as the hook point because it's -// called every time page renders, giving us a reliable hook to inject CSS +// We use refreshSignItemsForPage as the hook because Odoo calls this whenever +// a page is rendered (initial load AND during document navigation). patch(PDFIframe.prototype, { refreshSignItemsForPage(page) { - restoreBridgeAndForceShow(this, 'PDFIframe.refreshSignItemsForPage'); + // Guarantee connection before evaluating + restoreBridgeAndForceShow(this); try { const result = super.refreshSignItemsForPage(...arguments); @@ -118,81 +115,86 @@ patch(PDFIframe.prototype, { if (this.signItems && this.signItems[page]) { for (const id in this.signItems[page]) { const item = this.signItems[page][id]; - // Also forcefully remove d-none via JS just in case + + // Native safety net if (item && item.el) { item.el.classList.remove('d-none'); } - if (item && item.data && item.data.type === 'image' && item.el && !processedItems.has(item.el)) { - processedItems.add(item.el); - setupLazyObserver(item.el); + if (item && item.data && item.data.type === 'image' && item.el) { - // Add upload logic for active signer - if (!this.readonly && item.data.responsible > 0 && item.data.responsible === this.currentRole) { - const el = item.el; - const data = item.data; - el.setAttribute('data-type', 'image'); - const input = el.querySelector('.o_sign_image_upload_input'); - if (input && !el._hasImgListener) { - el.addEventListener('click', (e) => { - if (e.target !== input) input.click(); - }); - - input.addEventListener('change', (e) => { - const file = e.target.files[0]; - if (!file) return; + // Prevent multi-binding during rapid page flips + if (!item.el._odoo_lazy_configured) { + item.el._odoo_lazy_configured = true; + setupLazyObserver(item.el); + + // Add upload logic for active signer + if (!this.readonly && item.data.responsible > 0 && item.data.responsible === this.currentRole) { + const el = item.el; + const data = item.data; + el.setAttribute('data-type', 'image'); + const input = el.querySelector('.o_sign_image_upload_input'); + if (input && !el._hasImgListener) { + el.addEventListener('click', (e) => { + if (e.target !== input) input.click(); + }); + + input.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (!file) return; - const reader = new FileReader(); - reader.onload = (readerEvent) => { - const img = new Image(); - img.onload = () => { - const canvas = document.createElement('canvas'); - let width = img.width; - let height = img.height; - const MAX_SIZE = 1080; - if (width > height) { - if (width > MAX_SIZE) { - height *= MAX_SIZE / width; - width = MAX_SIZE; + const reader = new FileReader(); + reader.onload = (readerEvent) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + let width = img.width; + let height = img.height; + const MAX_SIZE = 1080; + if (width > height) { + if (width > MAX_SIZE) { + height *= MAX_SIZE / width; + width = MAX_SIZE; + } + } else if (height > MAX_SIZE) { + width *= MAX_SIZE / height; + height = MAX_SIZE; } - } else if (height > MAX_SIZE) { - width *= MAX_SIZE / height; - height = MAX_SIZE; - } - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0, width, height); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, width, height); - const compressedDataUrl = canvas.toDataURL('image/jpeg', 0.8); - data.value = compressedDataUrl; - el.dataset.value = compressedDataUrl; + const compressedDataUrl = canvas.toDataURL('image/jpeg', 0.8); + data.value = compressedDataUrl; + el.dataset.value = compressedDataUrl; - const placeholder = el.querySelector('.o_placeholder'); - if (placeholder) placeholder.remove(); + const placeholder = el.querySelector('.o_placeholder'); + if (placeholder) placeholder.remove(); - let existingImg = el.querySelector('img'); - if (!existingImg) { - existingImg = document.createElement('img'); - Object.assign(existingImg.style, { - maxWidth: '100%', - maxHeight: '100%', - objectFit: 'contain', - opacity: 1 - }); - const body = el.querySelector('.sign_item_body') || el; - body.appendChild(existingImg); - } - existingImg.src = compressedDataUrl; - el.classList.remove('o_sign_image_loading_container'); + let existingImg = el.querySelector('img'); + if (!existingImg) { + existingImg = document.createElement('img'); + Object.assign(existingImg.style, { + maxWidth: '100%', + maxHeight: '100%', + objectFit: 'contain', + opacity: 1 + }); + const body = el.querySelector('.sign_item_body') || el; + body.appendChild(existingImg); + } + existingImg.src = compressedDataUrl; + el.classList.remove('o_sign_image_loading_container'); - if (this.handleInput) this.handleInput(); + if (this.handleInput) this.handleInput(); + }; + img.src = readerEvent.target.result; }; - img.src = readerEvent.target.result; - }; - reader.readAsDataURL(file); - }); - el._hasImgListener = true; + reader.readAsDataURL(file); + }); + el._hasImgListener = true; + } } } } @@ -200,8 +202,7 @@ patch(PDFIframe.prototype, { } return result; } catch (error) { - console.error("[Diagnostic] Error inside refreshSignItemsForPage:", error); - // Even if super fails, don't crash our script + console.error("[Odoo PDF Fix] Error during refreshSignItemsForPage:", error); } } });