/** * Contact Forms - Theme Helper * * CSS conflict scanner that detects theme rules overriding Contact Forms styling. * Shows file, line number, selector, and conflicting properties. * * @package ContactForms * @since 2.0.0-beta.6 */ (function($) { 'use strict'; // Configuration from PHP var config = window.accuaThemeHelper || {}; var monitoredClasses = config.monitoredClasses || []; var criticalProperties = config.criticalProperties || []; var i18n = config.i18n || {}; var pluginCssUrl = config.pluginCssUrl || ''; // Build regex pattern for monitored classes var classPattern = new RegExp('\\b(' + monitoredClasses.join('|').replace(/-/g, '\\-') + ')\\b'); /** * Extract filename from URL */ function getFilename(url) { if (!url) return '(inline)'; try { var urlObj = new URL(url, window.location.origin); var pathname = urlObj.pathname; var filename = pathname.split('/').pop() || pathname; // Remove query string for display but keep for identification return filename.split('?')[0]; } catch (e) { return url.substring(0, 50) + '...'; } } // Remote site Contact Forms version info (set during URL scan) var remoteCfInfo = null; /** * Check if URL is the Contact Forms plugin CSS (skip it if same version) */ function isPluginCss(url) { if (!url) return false; // Match both old and new CSS paths var isPlugin = url.indexOf('contact-forms/assets/css/frontend.css') !== -1 || url.indexOf('contact-forms/assets/css/admin.css') !== -1 || url.indexOf('contact-forms/assets/css/accua-form-api.css') !== -1 || url.indexOf('contact-forms/accua-form-api.css') !== -1; // Old path return isPlugin; } /** * Check if plugin CSS should be skipped (same version as current) */ function shouldSkipPluginCss(url) { if (!isPluginCss(url)) return false; // If we have remote version info and it matches current, skip it if (remoteCfInfo && remoteCfInfo.remoteCssVersion) { return remoteCfInfo.remoteCssVersion === remoteCfInfo.currentCssVersion; } // For admin page scanning, always skip plugin CSS return true; } /** * Estimate line number from CSS text and rule position */ function estimateLineNumber(cssText, ruleText) { if (!cssText || !ruleText) return '?'; var selectorMatch = ruleText.match(/^[^{]+/); if (!selectorMatch) return '?'; var selector = selectorMatch[0].trim(); var index = cssText.indexOf(selector); if (index === -1) return '?'; // Count newlines before this position var textBefore = cssText.substring(0, index); var lineNumber = (textBefore.match(/\n/g) || []).length + 1; return lineNumber; } /** * Parse a CSS rule and extract conflicting properties */ function parseConflictingProperties(rule) { var conflicts = []; var style = rule.style; if (!style) return conflicts; for (var i = 0; i < style.length; i++) { var prop = style[i]; // Check if this property is in our critical list if (criticalProperties.indexOf(prop) !== -1) { var value = style.getPropertyValue(prop); var priority = style.getPropertyPriority(prop); conflicts.push({ property: prop, value: value, important: priority === 'important' }); } } return conflicts; } /** * Format conflicting properties for display */ function formatConflicts(conflicts) { return conflicts.map(function(c) { var important = c.important ? ' !important' : ''; return '' + c.property + ': ' + c.value + important + ''; }).join('
'); } /** * Scan a single stylesheet for conflicts */ function scanStylesheet(sheet, cssText) { var results = []; var url = sheet.href || ''; // Skip Contact Forms own CSS if same version if (shouldSkipPluginCss(url)) { return results; } try { var rules = sheet.cssRules || sheet.rules || []; for (var i = 0; i < rules.length; i++) { var rule = rules[i]; // Only process style rules (not @media, @keyframes, etc.) if (rule.type !== CSSRule.STYLE_RULE) continue; var selector = rule.selectorText || ''; // Check if selector contains any monitored class if (!classPattern.test(selector)) continue; // Get conflicting properties var conflicts = parseConflictingProperties(rule); if (conflicts.length > 0) { // Estimate line number var lineNumber = '?'; if (cssText) { lineNumber = estimateLineNumber(cssText, rule.cssText); } results.push({ url: url, filename: getFilename(url), line: lineNumber, selector: selector, conflicts: conflicts, ruleText: rule.cssText }); } } } catch (e) { // Cross-origin stylesheets throw security errors console.log('Cannot access stylesheet:', url, e.message); } return results; } /** * Scan CSS text string for conflicts (for URL scanning) */ function scanCssText(cssText, sourceUrl) { var results = []; // Skip Contact Forms own CSS if same version if (shouldSkipPluginCss(sourceUrl)) { return results; } // Parse CSS text manually with regex // Match rules: selector { properties } var rulePattern = /([^{}]+)\{([^{}]+)\}/g; var match; var position = 0; while ((match = rulePattern.exec(cssText)) !== null) { var selector = match[1].trim(); var properties = match[2].trim(); // Check if selector contains any monitored class if (!classPattern.test(selector)) continue; // Parse properties var conflicts = []; var propPattern = /([\w-]+)\s*:\s*([^;]+);?/g; var propMatch; while ((propMatch = propPattern.exec(properties)) !== null) { var prop = propMatch[1].trim(); var value = propMatch[2].trim(); if (criticalProperties.indexOf(prop) !== -1) { var isImportant = value.indexOf('!important') !== -1; value = value.replace(/\s*!important\s*/i, ''); conflicts.push({ property: prop, value: value, important: isImportant }); } } if (conflicts.length > 0) { // Estimate line number var textBefore = cssText.substring(0, match.index); var lineNumber = (textBefore.match(/\n/g) || []).length + 1; results.push({ url: sourceUrl, filename: getFilename(sourceUrl), line: lineNumber, selector: selector, conflicts: conflicts, ruleText: match[0] }); } } return results; } /** * Scan all stylesheets on current page */ function scanCurrentPage() { var allResults = []; var sheets = document.styleSheets; for (var i = 0; i < sheets.length; i++) { var sheet = sheets[i]; // Try to get CSS text for line number estimation var cssText = ''; try { if (sheet.cssRules) { cssText = Array.from(sheet.cssRules).map(function(r) { return r.cssText; }).join('\n'); } } catch (e) { // Cross-origin } var results = scanStylesheet(sheet, cssText); allResults = allResults.concat(results); } return allResults; } /** * Display results in the table */ function displayResults(results) { var $table = $('#theme-helper-results'); var $tbody = $('#conflicts-tbody'); var $summary = $('#results-summary'); $tbody.empty(); if (results.length === 0) { $summary.html( '✓ ' + i18n.noConflicts + '' + '

All scanned stylesheets appear compatible with Contact Forms.

' ); $table.show(); return; } // Group by file var byFile = {}; results.forEach(function(r) { var key = r.filename; if (!byFile[key]) byFile[key] = []; byFile[key].push(r); }); var fileCount = Object.keys(byFile).length; $summary.html( '⚠ ' + i18n.conflictsFound + '' + '

' + results.length + ' conflicting rule(s) found in ' + fileCount + ' file(s).' + '

' ); // Populate table results.forEach(function(r) { var $row = $(''); // Stylesheet column - make it a clickable link var urlTitle = r.url || '(unknown)'; var filenameHtml; if (r.url && r.url.indexOf('(inline') === -1) { // External URL - make it clickable filenameHtml = '' + '' + r.filename + ' '; } else { filenameHtml = '' + r.filename + ''; } $row.append('' + filenameHtml + ''); // Line column $row.append('' + r.line + ''); // Selector column $row.append('' + r.selector + ''); // Properties column $row.append('' + formatConflicts(r.conflicts) + ''); $tbody.append($row); }); $table.show(); } /** * Show status message */ function showStatus(message, type) { var $status = $('#theme-helper-status'); var borderColor = type === 'error' ? '#d63638' : (type === 'success' ? '#00a32a' : '#0073aa'); var bgColor = type === 'error' ? '#fcf0f1' : (type === 'success' ? '#edfaef' : '#f0f6fc'); $status.css({ 'border-left-color': borderColor, 'background': bgColor }); $('#status-message').html(message); $status.show(); } /** * Scan current page button handler */ function handleScanCurrentPage() { showStatus(' ' + i18n.scanning, 'info'); // Small delay to allow spinner to render setTimeout(function() { var results = scanCurrentPage(); displayResults(results); showStatus('✓ ' + i18n.scanComplete + ' - ' + results.length + ' conflict(s) found', results.length > 0 ? 'error' : 'success'); }, 100); } /** * Scan URL button handler */ function handleScanUrl() { var url = $('#scan-url').val().trim(); if (!url) { showStatus('⚠ ' + i18n.enterUrl, 'error'); return; } showStatus(' ' + i18n.fetchingCss, 'info'); $.ajax({ url: config.ajaxUrl, method: 'POST', data: { action: 'accua_forms_fetch_url_css', nonce: config.nonce, url: url }, success: function(response) { if (!response.success) { showStatus('⚠ ' + (response.data?.message || i18n.scanError), 'error'); return; } var stylesheets = response.data.stylesheets || []; var allResults = []; // Store remote CF version info for version comparison remoteCfInfo = { remoteCssVersion: response.data.remoteCssVersion, remoteVersion: response.data.remoteContactFormsVersion, currentCssVersion: response.data.currentCssVersion, currentVersion: response.data.currentVersion }; // Scan each stylesheet stylesheets.forEach(function(sheet) { var results = scanCssText(sheet.content, sheet.url); allResults = allResults.concat(results); }); // Build version info message var versionInfo = ''; if (remoteCfInfo.remoteCssVersion && remoteCfInfo.remoteCssVersion !== remoteCfInfo.currentCssVersion) { versionInfo = '
⚠ Remote site uses Contact Forms CSS v' + remoteCfInfo.remoteCssVersion + ' (you have v' + remoteCfInfo.currentCssVersion + ')'; } else if (remoteCfInfo.remoteCssVersion) { versionInfo = '
✓ Remote site uses same Contact Forms CSS version (v' + remoteCfInfo.remoteCssVersion + ')'; } displayResults(allResults); showStatus('✓ ' + i18n.scanComplete + ' - Scanned ' + stylesheets.length + ' stylesheet(s), ' + allResults.length + ' conflict(s) found' + versionInfo, allResults.length > 0 ? 'error' : 'success'); }, error: function() { showStatus('⚠ ' + i18n.scanError, 'error'); } }); } // Initialize on document ready $(function() { $('#scan-current-page').on('click', handleScanCurrentPage); $('#scan-url-button').on('click', handleScanUrl); $('#scan-url').on('keypress', function(e) { if (e.which === 13) { handleScanUrl(); } }); }); })(jQuery);