fix bug overlap with OWL DOM

This commit is contained in:
Suherdy Yacob 2026-01-09 11:24:49 +07:00
parent 086bd6a944
commit ea0fee1b45
3 changed files with 284 additions and 197 deletions

View File

@ -1,109 +1,146 @@
/** @odoo-module **/ /** @odoo-module **/
console.log('Loading comprehensive decimal observer...'); console.log('Loading safe decimal observer...');
// Function to apply decimal styling to any element // Function to safely apply decimal styling to any element
function applyDecimalStyling(element) { function safelyApplyDecimalStyling(element) {
if (!element || !element.textContent) return; if (!element || !element.textContent || !document.contains(element)) return false;
// Skip if already processed // Skip if already processed or if Owl is updating this element
if (element.querySelector('.o_decimal') || element.classList.contains('o_decimal')) return; 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(); try {
if (!text) return; const text = element.textContent.trim();
if (!text) return false;
// Pattern for decimal numbers (including currency formats)
const patterns = [ // Mark as processed to avoid conflicts
// Standard decimal: 123.45, 1,234.56 element.setAttribute('data-decimal-processed', 'true');
/^(.*)(\d{1,3}(?:[.,]\d{3})*)[.,](\d+)(.*)$/,
// Currency with Rp: Rp 6,210,000.00 // Pattern for decimal numbers (including currency formats)
/^(.*Rp\s*)(\d{1,3}(?:[.,]\d{3})*)[.,](\d+)(.*)$/, const patterns = [
// Simple decimal: 240.000 // Currency with Rp: Rp 6,210,000.00
/^(.*)(\d+)[.](\d+)(.*)$/ /^(.*Rp\s*)(\d{1,3}(?:[.,]\d{3})*)[.,](\d+)(.*)$/,
]; // Standard decimal: 123.45, 1,234.56
/^(.*)(\d{1,3}(?:[.,]\d{3})*)[.,](\d+)(.*)$/,
for (const pattern of patterns) { // Simple decimal: 240.000
const match = text.match(pattern); /^(.*)(\d+)[.](\d+)(.*)$/
if (match) { ];
const [, prefix, integerPart, decimalPart, suffix] = match;
for (const pattern of patterns) {
console.log('Decimal Observer: Styling', text); const match = text.match(pattern);
if (match) {
// Create new HTML with decimal styling const [, prefix, integerPart, decimalPart, suffix] = match;
const newHTML = `${prefix || ''}${integerPart}<span class="o_decimal">.${decimalPart}</span>${suffix || ''}`;
element.innerHTML = newHTML; console.log('Safe Decimal Observer: Styling', text);
return true;
// Double-check the element hasn't changed before updating
if (element.textContent.trim() === text && document.contains(element)) {
const newHTML = `${prefix || ''}${integerPart}<span class="o_decimal">.${decimalPart}</span>${suffix || ''}`;
element.innerHTML = newHTML;
return true;
}
}
} }
}
// Handle whole numbers in quantity contexts (be more selective)
// Handle whole numbers in quantity contexts if (text.match(/^\d+$/) && text.length > 0 && (
if (text.match(/^\d+$/) && ( element.closest('[name="quantity"]') ||
element.closest('[name="quantity"]') || (element.closest('td') && element.closest('table') && text.length <= 6) // Only for reasonable quantities
element.closest('.o_field_float') || )) {
element.closest('td') && element.closest('table') console.log('Safe Decimal Observer: Adding decimals to whole number', text);
)) {
console.log('Decimal Observer: Adding decimals to whole number', text); // Double-check before updating
element.innerHTML = `${text}<span class="o_decimal">.000</span>`; if (element.textContent.trim() === text && document.contains(element)) {
return true; element.innerHTML = `${text}<span class="o_decimal">.000</span>`;
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; return false;
} }
// Process all elements in a container // Safe processing of containers
function processContainer(container) { function safelyProcessContainer(container) {
if (!container) return; if (!container || !document.contains(container)) return;
// Find all text-containing elements try {
const elements = container.querySelectorAll('td, span, div, .o_field_monetary, .o_field_float'); // 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 (no child elements with text) elements.forEach(element => {
if (element.children.length === 0 || // Only process leaf elements and skip if Owl is managing them
(element.children.length === 1 && element.children[0].tagName === 'SPAN')) { if ((element.children.length === 0 ||
applyDecimalStyling(element); (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 // More conservative observer
const observer = new MutationObserver((mutations) => { const safeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => { // Debounce the processing to avoid conflicts
if (mutation.type === 'childList') { clearTimeout(safeObserver.timeout);
mutation.addedNodes.forEach((node) => { safeObserver.timeout = setTimeout(() => {
if (node.nodeType === Node.ELEMENT_NODE) { try {
// Process the new node and its children mutations.forEach((mutation) => {
processContainer(node); 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 // Start observing safely
function startObserver() { function startSafeObserver() {
console.log('Decimal Observer: Starting comprehensive observation'); console.log('Safe Decimal Observer: Starting conservative observation');
// Process existing content // Process existing content safely
processContainer(document.body); requestAnimationFrame(() => {
safelyProcessContainer(document.body);
});
// Start observing for new content // Start observing for new content with reduced frequency
observer.observe(document.body, { safeObserver.observe(document.body, {
childList: true, childList: true,
subtree: true subtree: true
}); });
// Also process content periodically for dynamic updates // Periodic processing with longer intervals to reduce conflicts
setInterval(() => { setInterval(() => {
processContainer(document.body); requestAnimationFrame(() => {
}, 2000); safelyProcessContainer(document.body);
});
}, 5000); // Every 5 seconds instead of 2
} }
// Start immediately or when DOM is ready // Start safely
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startObserver); document.addEventListener('DOMContentLoaded', startSafeObserver);
} else { } else {
startObserver(); startSafeObserver();
} }
console.log('Comprehensive decimal observer loaded'); console.log('Safe decimal observer loaded');

View File

@ -7,103 +7,129 @@ import { formatMonetary, formatFloat } from "@web/views/fields/formatters";
console.log('Loading Web Decimal Style formatters patch...'); console.log('Loading Web Decimal Style formatters patch...');
// Patch Components for Form Views // Safe field patching with error handling
patch(MonetaryField.prototype, { try {
get formattedValue() { // Patch Components for Form Views
if (this.props.inputType === "number" && !this.props.readonly && this.value) { patch(MonetaryField.prototype, {
return this.value; get formattedValue() {
} if (this.props.inputType === "number" && !this.props.readonly && this.value) {
const res = formatMonetary(this.value, { return this.value;
digits: this.currencyDigits, }
currencyId: this.currencyId, const res = formatMonetary(this.value, {
noSymbol: !this.props.readonly || this.props.hideSymbol, 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}<span class="o_decimal">.${decimalPart}</span>${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,
}); });
} 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}<span class="o_decimal">.${decimalPart}</span>${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 // For readonly fields, apply decimal styling via safe DOM manipulation
// This avoids markup escaping issues 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}<span class="o_decimal">.${decimalPart}</span>${suffix}`;
}
}
} catch (error) {
console.warn('Monetary field decimal styling error (non-critical):', error);
}
}, 150); // Longer delay to avoid Owl conflicts
}
return res; 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}<span class="o_decimal">.${decimalPart}</span>${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 // Safe Registry Patching for List Views
['float', 'monetary', 'percentage', 'float_factor', 'float_time'].forEach(patchFormatter); 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'); console.log('Web Decimal Style formatters patch loaded successfully');

View File

@ -5,47 +5,71 @@ import { ListRenderer } from "@web/views/list/list_renderer";
console.log('Loading inventory decimal patch...'); 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, { patch(ListRenderer.prototype, {
async render() { async render() {
const result = await super.render(); const result = await super.render();
// After rendering, find and modify decimal numbers in the DOM // Schedule decimal styling after Owl completes its rendering cycle
setTimeout(() => { this.scheduleDecimalStyling();
this.applyDecimalStyling();
}, 100);
return result; 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() { applyDecimalStyling() {
if (!this.el) return; if (!this.el || !document.contains(this.el)) return;
// Find all cells in the list table // Find all cells in the list table, but be more careful about DOM manipulation
const cells = this.el.querySelectorAll('td, .o_data_cell'); const cells = this.el.querySelectorAll('td:not([data-decimal-processed]), .o_data_cell:not([data-decimal-processed])');
cells.forEach(cell => { cells.forEach(cell => {
// Skip if already processed try {
if (cell.querySelector('.o_decimal')) return; // Skip if cell is being updated by Owl
if (cell.hasAttribute('data-owl-updating')) 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;
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 text = cell.textContent?.trim();
const newHTML = `${prefix}${integerPart}<span class="o_decimal">${decimalPoint}${decimalPart}</span>${suffix}`; if (!text) return;
cell.innerHTML = newHTML;
} // Check for decimal numbers (including currency)
// Handle whole numbers by adding .000 const decimalMatch = text.match(/^(.*)(\d+)([.,])(\d+)(.*)$/);
else if (text.match(/^\d+$/)) { if (decimalMatch) {
console.log('Adding decimals to whole number:', text); const [, prefix, integerPart, decimalPoint, decimalPart, suffix] = decimalMatch;
cell.innerHTML = `${text}<span class="o_decimal">.000</span>`;
console.log('Applying safe DOM decimal styling to:', text);
// Create new HTML structure safely
const newHTML = `${prefix}${integerPart}<span class="o_decimal">${decimalPoint}${decimalPart}</span>${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}<span class="o_decimal">.000</span>`;
}
}
} catch (error) {
console.warn('Error processing cell (non-critical):', error);
} }
}); });
} }