From ea0fee1b454fb75177f3980cd1a987a50ded8e75 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Fri, 9 Jan 2026 11:24:49 +0700 Subject: [PATCH] fix bug overlap with OWL DOM --- static/src/views/decimal_observer.js | 191 +++++++++++------- static/src/views/fields/formatters_patch.js | 212 +++++++++++--------- static/src/views/inventory_decimal_patch.js | 78 ++++--- 3 files changed, 284 insertions(+), 197 deletions(-) diff --git a/static/src/views/decimal_observer.js b/static/src/views/decimal_observer.js index 6569a18..4087602 100644 --- a/static/src/views/decimal_observer.js +++ b/static/src/views/decimal_observer.js @@ -1,109 +1,146 @@ /** @odoo-module **/ -console.log('Loading comprehensive decimal observer...'); +console.log('Loading safe decimal observer...'); -// Function to apply decimal styling to any element -function applyDecimalStyling(element) { - if (!element || !element.textContent) return; +// Function to safely apply decimal styling to any element +function safelyApplyDecimalStyling(element) { + if (!element || !element.textContent || !document.contains(element)) return false; - // Skip if already processed - if (element.querySelector('.o_decimal') || element.classList.contains('o_decimal')) return; + // Skip if already processed or if Owl is updating this element + if (element.querySelector('.o_decimal') || + element.classList.contains('o_decimal') || + element.hasAttribute('data-decimal-processed') || + element.hasAttribute('data-owl-updating')) return false; - const text = element.textContent.trim(); - if (!text) return; - - // Pattern for decimal numbers (including currency formats) - const patterns = [ - // Standard decimal: 123.45, 1,234.56 - /^(.*)(\d{1,3}(?:[.,]\d{3})*)[.,](\d+)(.*)$/, - // Currency with Rp: Rp 6,210,000.00 - /^(.*Rp\s*)(\d{1,3}(?:[.,]\d{3})*)[.,](\d+)(.*)$/, - // Simple decimal: 240.000 - /^(.*)(\d+)[.](\d+)(.*)$/ - ]; - - for (const pattern of patterns) { - const match = text.match(pattern); - if (match) { - const [, prefix, integerPart, decimalPart, suffix] = match; - - console.log('Decimal Observer: Styling', text); - - // Create new HTML with decimal styling - const newHTML = `${prefix || ''}${integerPart}.${decimalPart}${suffix || ''}`; - element.innerHTML = newHTML; - return true; + try { + const text = element.textContent.trim(); + if (!text) return false; + + // Mark as processed to avoid conflicts + element.setAttribute('data-decimal-processed', 'true'); + + // Pattern for decimal numbers (including currency formats) + const patterns = [ + // Currency with Rp: Rp 6,210,000.00 + /^(.*Rp\s*)(\d{1,3}(?:[.,]\d{3})*)[.,](\d+)(.*)$/, + // Standard decimal: 123.45, 1,234.56 + /^(.*)(\d{1,3}(?:[.,]\d{3})*)[.,](\d+)(.*)$/, + // Simple decimal: 240.000 + /^(.*)(\d+)[.](\d+)(.*)$/ + ]; + + for (const pattern of patterns) { + const match = text.match(pattern); + if (match) { + const [, prefix, integerPart, decimalPart, suffix] = match; + + console.log('Safe Decimal Observer: Styling', text); + + // Double-check the element hasn't changed before updating + if (element.textContent.trim() === text && document.contains(element)) { + const newHTML = `${prefix || ''}${integerPart}.${decimalPart}${suffix || ''}`; + element.innerHTML = newHTML; + return true; + } + } } - } - - // Handle whole numbers in quantity contexts - if (text.match(/^\d+$/) && ( - element.closest('[name="quantity"]') || - element.closest('.o_field_float') || - element.closest('td') && element.closest('table') - )) { - console.log('Decimal Observer: Adding decimals to whole number', text); - element.innerHTML = `${text}.000`; - return true; + + // Handle whole numbers in quantity contexts (be more selective) + if (text.match(/^\d+$/) && text.length > 0 && ( + element.closest('[name="quantity"]') || + (element.closest('td') && element.closest('table') && text.length <= 6) // Only for reasonable quantities + )) { + console.log('Safe Decimal Observer: Adding decimals to whole number', text); + + // Double-check before updating + if (element.textContent.trim() === text && document.contains(element)) { + element.innerHTML = `${text}.000`; + return true; + } + } + + } catch (error) { + console.warn('Safe decimal styling error (non-critical):', error); + // Remove the processing flag if there was an error + element.removeAttribute('data-decimal-processed'); } return false; } -// Process all elements in a container -function processContainer(container) { - if (!container) return; +// Safe processing of containers +function safelyProcessContainer(container) { + if (!container || !document.contains(container)) return; - // Find all text-containing elements - const elements = container.querySelectorAll('td, span, div, .o_field_monetary, .o_field_float'); - - elements.forEach(element => { - // Only process leaf elements (no child elements with text) - if (element.children.length === 0 || - (element.children.length === 1 && element.children[0].tagName === 'SPAN')) { - applyDecimalStyling(element); - } - }); + try { + // Find all text-containing elements, but be more selective + const elements = container.querySelectorAll('td:not([data-decimal-processed]), span:not([data-decimal-processed]), .o_field_monetary:not([data-decimal-processed]), .o_field_float:not([data-decimal-processed])'); + + elements.forEach(element => { + // Only process leaf elements and skip if Owl is managing them + if ((element.children.length === 0 || + (element.children.length === 1 && element.children[0].tagName === 'SPAN')) && + !element.hasAttribute('data-owl-updating')) { + safelyApplyDecimalStyling(element); + } + }); + } catch (error) { + console.warn('Safe container processing error (non-critical):', error); + } } -// Observer for dynamic content -const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === 'childList') { - mutation.addedNodes.forEach((node) => { - if (node.nodeType === Node.ELEMENT_NODE) { - // Process the new node and its children - processContainer(node); +// More conservative observer +const safeObserver = new MutationObserver((mutations) => { + // Debounce the processing to avoid conflicts + clearTimeout(safeObserver.timeout); + safeObserver.timeout = setTimeout(() => { + try { + mutations.forEach((mutation) => { + if (mutation.type === 'childList') { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE && document.contains(node)) { + // Use requestAnimationFrame to ensure we run after Owl updates + requestAnimationFrame(() => { + safelyProcessContainer(node); + }); + } + }); } }); + } catch (error) { + console.warn('Safe observer error (non-critical):', error); } - }); + }, 200); // Debounce for 200ms }); -// Start observing when DOM is ready -function startObserver() { - console.log('Decimal Observer: Starting comprehensive observation'); +// Start observing safely +function startSafeObserver() { + console.log('Safe Decimal Observer: Starting conservative observation'); - // Process existing content - processContainer(document.body); + // Process existing content safely + requestAnimationFrame(() => { + safelyProcessContainer(document.body); + }); - // Start observing for new content - observer.observe(document.body, { + // Start observing for new content with reduced frequency + safeObserver.observe(document.body, { childList: true, subtree: true }); - // Also process content periodically for dynamic updates + // Periodic processing with longer intervals to reduce conflicts setInterval(() => { - processContainer(document.body); - }, 2000); + requestAnimationFrame(() => { + safelyProcessContainer(document.body); + }); + }, 5000); // Every 5 seconds instead of 2 } -// Start immediately or when DOM is ready +// Start safely if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', startObserver); + document.addEventListener('DOMContentLoaded', startSafeObserver); } else { - startObserver(); + startSafeObserver(); } -console.log('Comprehensive decimal observer loaded'); \ No newline at end of file +console.log('Safe decimal observer loaded'); \ No newline at end of file diff --git a/static/src/views/fields/formatters_patch.js b/static/src/views/fields/formatters_patch.js index 621df94..6eb0172 100644 --- a/static/src/views/fields/formatters_patch.js +++ b/static/src/views/fields/formatters_patch.js @@ -7,103 +7,129 @@ import { formatMonetary, formatFloat } from "@web/views/fields/formatters"; console.log('Loading Web Decimal Style formatters patch...'); -// Patch Components for Form Views -patch(MonetaryField.prototype, { - get formattedValue() { - if (this.props.inputType === "number" && !this.props.readonly && this.value) { - return this.value; - } - const res = formatMonetary(this.value, { - digits: this.currencyDigits, - currencyId: this.currencyId, - noSymbol: !this.props.readonly || this.props.hideSymbol, - }); - - // For readonly fields, apply decimal styling via DOM manipulation - if (this.props.readonly) { - setTimeout(() => { - const element = this.el?.querySelector('.o_field_monetary'); - if (element && !element.querySelector('.o_decimal')) { - const text = element.textContent; - const match = text?.match(/^(.*)(\d{1,3}(?:[.,]\d{3})*)[.,](\d+)(.*)$/); - if (match) { - const [, prefix, integerPart, decimalPart, suffix] = match; - element.innerHTML = `${prefix}${integerPart}.${decimalPart}${suffix}`; - } - } - }, 50); - } - - return res; - } -}); - -patch(FloatField.prototype, { - get formattedValue() { - if ( - !this.props.formatNumber || - (this.props.inputType === "number" && !this.props.readonly && this.value) - ) { - return this.value; - } - const options = { - digits: this.props.digits, - field: this.props.record.fields[this.props.name], - }; - let res; - if (this.props.humanReadable && !this.state.hasFocus) { - res = formatFloat(this.value, { - ...options, - humanReadable: true, - decimals: this.props.decimals, +// Safe field patching with error handling +try { + // Patch Components for Form Views + patch(MonetaryField.prototype, { + get formattedValue() { + if (this.props.inputType === "number" && !this.props.readonly && this.value) { + return this.value; + } + const res = formatMonetary(this.value, { + digits: this.currencyDigits, + currencyId: this.currencyId, + noSymbol: !this.props.readonly || this.props.hideSymbol, }); - } else { - res = formatFloat(this.value, { ...options, humanReadable: false }); - } - - // For readonly fields, apply decimal styling via DOM manipulation - if (this.props.readonly) { - setTimeout(() => { - const element = this.el?.querySelector('.o_field_float'); - if (element && !element.querySelector('.o_decimal')) { - const text = element.textContent; - const match = text?.match(/^(.*)(\d+)[.](\d+)(.*)$/); - if (match) { - const [, prefix, integerPart, decimalPart, suffix] = match; - element.innerHTML = `${prefix}${integerPart}.${decimalPart}${suffix}`; - } - } - }, 50); - } - - return res; - } -}); - -// Enhanced Registry Patching for List Views -const formattersRegistry = registry.category("formatters"); - -// Store original formatters -const originalFormatters = new Map(); - -function patchFormatter(name) { - if (formattersRegistry.contains(name) && !originalFormatters.has(name)) { - const original = formattersRegistry.get(name); - originalFormatters.set(name, original); - - formattersRegistry.add(name, (...args) => { - const res = original(...args); - // Return the result as-is, let DOM observer handle styling - // This avoids markup escaping issues + // For readonly fields, apply decimal styling via safe DOM manipulation + if (this.props.readonly) { + // Use a longer timeout to avoid Owl lifecycle conflicts + setTimeout(() => { + try { + const element = this.el?.querySelector('.o_field_monetary'); + if (element && !element.querySelector('.o_decimal') && document.contains(element)) { + const text = element.textContent; + const match = text?.match(/^(.*)(\d{1,3}(?:[.,]\d{3})*)[.,](\d+)(.*)$/); + if (match && element.textContent === text) { // Double-check content hasn't changed + const [, prefix, integerPart, decimalPart, suffix] = match; + element.innerHTML = `${prefix}${integerPart}.${decimalPart}${suffix}`; + } + } + } catch (error) { + console.warn('Monetary field decimal styling error (non-critical):', error); + } + }, 150); // Longer delay to avoid Owl conflicts + } + return res; - }, { force: true }); - - console.log('Web Decimal Style: Patched formatter', name); - } + } + }); + + patch(FloatField.prototype, { + get formattedValue() { + if ( + !this.props.formatNumber || + (this.props.inputType === "number" && !this.props.readonly && this.value) + ) { + return this.value; + } + const options = { + digits: this.props.digits, + field: this.props.record.fields[this.props.name], + }; + let res; + if (this.props.humanReadable && !this.state.hasFocus) { + res = formatFloat(this.value, { + ...options, + humanReadable: true, + decimals: this.props.decimals, + }); + } else { + res = formatFloat(this.value, { ...options, humanReadable: false }); + } + + // For readonly fields, apply decimal styling via safe DOM manipulation + if (this.props.readonly) { + setTimeout(() => { + try { + const element = this.el?.querySelector('.o_field_float'); + if (element && !element.querySelector('.o_decimal') && document.contains(element)) { + const text = element.textContent; + const match = text?.match(/^(.*)(\d+)[.](\d+)(.*)$/); + if (match && element.textContent === text) { // Double-check content hasn't changed + const [, prefix, integerPart, decimalPart, suffix] = match; + element.innerHTML = `${prefix}${integerPart}.${decimalPart}${suffix}`; + } + } + } catch (error) { + console.warn('Float field decimal styling error (non-critical):', error); + } + }, 150); + } + + return res; + } + }); +} catch (error) { + console.warn('Field patching error (non-critical):', error); } -// Patch all numeric formatters -['float', 'monetary', 'percentage', 'float_factor', 'float_time'].forEach(patchFormatter); +// Safe Registry Patching for List Views +try { + const formattersRegistry = registry.category("formatters"); + + // Store original formatters + const originalFormatters = new Map(); + + function safelyPatchFormatter(name) { + if (formattersRegistry.contains(name) && !originalFormatters.has(name)) { + try { + const original = formattersRegistry.get(name); + originalFormatters.set(name, original); + + formattersRegistry.add(name, (...args) => { + try { + const res = original(...args); + // Return the result as-is, let DOM observer handle styling + // This avoids markup escaping issues and Owl conflicts + return res; + } catch (error) { + console.warn(`Formatter ${name} error (non-critical):`, error); + return original(...args); // Fallback to original + } + }, { force: true }); + + console.log('Web Decimal Style: Safely patched formatter', name); + } catch (error) { + console.warn(`Error patching formatter ${name} (non-critical):`, error); + } + } + } + + // Patch all numeric formatters safely + ['float', 'monetary', 'percentage', 'float_factor', 'float_time'].forEach(safelyPatchFormatter); +} catch (error) { + console.warn('Registry patching error (non-critical):', error); +} console.log('Web Decimal Style formatters patch loaded successfully'); diff --git a/static/src/views/inventory_decimal_patch.js b/static/src/views/inventory_decimal_patch.js index 2fc0c4f..3d7dbff 100644 --- a/static/src/views/inventory_decimal_patch.js +++ b/static/src/views/inventory_decimal_patch.js @@ -5,47 +5,71 @@ import { ListRenderer } from "@web/views/list/list_renderer"; console.log('Loading inventory decimal patch...'); -// Post-render DOM manipulation approach to avoid markup escaping issues +// Safe DOM manipulation approach to avoid Owl lifecycle conflicts patch(ListRenderer.prototype, { async render() { const result = await super.render(); - // After rendering, find and modify decimal numbers in the DOM - setTimeout(() => { - this.applyDecimalStyling(); - }, 100); + // Schedule decimal styling after Owl completes its rendering cycle + this.scheduleDecimalStyling(); return result; }, + scheduleDecimalStyling() { + // Use requestAnimationFrame to ensure we run after Owl's DOM updates + requestAnimationFrame(() => { + try { + this.applyDecimalStyling(); + } catch (error) { + console.warn('Decimal styling error (non-critical):', error); + } + }); + }, + applyDecimalStyling() { - if (!this.el) return; + if (!this.el || !document.contains(this.el)) return; - // Find all cells in the list table - const cells = this.el.querySelectorAll('td, .o_data_cell'); + // Find all cells in the list table, but be more careful about DOM manipulation + const cells = this.el.querySelectorAll('td:not([data-decimal-processed]), .o_data_cell:not([data-decimal-processed])'); cells.forEach(cell => { - // Skip if already processed - if (cell.querySelector('.o_decimal')) return; - - const text = cell.textContent?.trim(); - if (!text) return; - - // Check for decimal numbers (including currency) - const decimalMatch = text.match(/^(.*)(\d+)([.,])(\d+)(.*)$/); - if (decimalMatch) { - const [, prefix, integerPart, decimalPoint, decimalPart, suffix] = decimalMatch; + try { + // Skip if cell is being updated by Owl + if (cell.hasAttribute('data-owl-updating')) return; - console.log('Applying DOM decimal styling to:', text); + // Mark as processed to avoid double processing + cell.setAttribute('data-decimal-processed', 'true'); - // Create new HTML structure - const newHTML = `${prefix}${integerPart}${decimalPoint}${decimalPart}${suffix}`; - cell.innerHTML = newHTML; - } - // Handle whole numbers by adding .000 - else if (text.match(/^\d+$/)) { - console.log('Adding decimals to whole number:', text); - cell.innerHTML = `${text}.000`; + const text = cell.textContent?.trim(); + if (!text) return; + + // Check for decimal numbers (including currency) + const decimalMatch = text.match(/^(.*)(\d+)([.,])(\d+)(.*)$/); + if (decimalMatch) { + const [, prefix, integerPart, decimalPoint, decimalPart, suffix] = decimalMatch; + + console.log('Applying safe DOM decimal styling to:', text); + + // Create new HTML structure safely + const newHTML = `${prefix}${integerPart}${decimalPoint}${decimalPart}${suffix}`; + + // Only update if the content hasn't changed + if (cell.textContent.trim() === text) { + cell.innerHTML = newHTML; + } + } + // Handle whole numbers by adding .000 + else if (text.match(/^\d+$/) && text.length > 0) { + console.log('Adding decimals to whole number safely:', text); + + // Only update if the content hasn't changed + if (cell.textContent.trim() === text) { + cell.innerHTML = `${text}.000`; + } + } + } catch (error) { + console.warn('Error processing cell (non-critical):', error); } }); }