/**
* 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 = $('' + r.selector + '