Refactor script injection to type="text/plain" inert pattern

Custom scripts are now rendered as inert <script type="text/plain"
data-att-cc-category="..."> tags in the page HTML. On consent,
consent-manager.js scans the DOM and activates matching elements.
This replaces the JSON-in-config approach and allows third-party
plugins (e.g. HFCM) to output consent-gated scripts using the
same data attribute convention.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 09:28:11 +00:00
parent 256a4a9dec
commit 0a73181ea7
4 changed files with 131 additions and 22 deletions

View File

@@ -129,32 +129,68 @@ var AttConsent = (function() {
/**
* Execute custom scripts for the consented categories.
* Scans the DOM for inert type="text/plain" script tags and template tags
* with matching data-att-cc-category attributes, then activates them.
*
* @param {Object} categories Consent categories.
*/
function executeConsentedScripts(categories) {
var scripts = config.scripts || {};
var cats = Object.keys(categories).filter(function(cat) {
return categories[cat];
});
if (!cats.length) return;
Object.keys(categories).forEach(function(cat) {
if (!categories[cat] || !scripts[cat]) {
var selectors = [];
cats.forEach(function(cat) {
selectors.push('script[type="text/plain"][data-att-cc-category="' + cat + '"]');
selectors.push('template[data-att-cc-category="' + cat + '"][data-att-cc-type="html"]');
});
var elements = document.querySelectorAll(selectors.join(','));
Array.prototype.slice.call(elements).forEach(function(el) {
if (el.nodeName === 'SCRIPT') {
activateScript(el);
} else if (el.nodeName === 'TEMPLATE') {
activateTemplate(el);
}
});
}
/**
* Activate an inert script element by creating a fresh <script> tag.
* Copies all attributes except type and data-att-cc-category,
* then removes the original to prevent double execution.
*
* @param {HTMLScriptElement} blocked The inert script element.
*/
function activateScript(blocked) {
var script = document.createElement('script');
Array.prototype.slice.call(blocked.attributes).forEach(function(attr) {
if (attr.name === 'type' || attr.name === 'data-att-cc-category') {
return;
}
['head', 'footer'].forEach(function(placement) {
if (!scripts[cat][placement] || !scripts[cat][placement].length) {
return;
}
var container = placement === 'head' ? document.head : document.body;
scripts[cat][placement].forEach(function(snippet) {
injectHTML(snippet, container);
});
// Clear to prevent double execution.
scripts[cat][placement] = [];
});
script.setAttribute(attr.name, attr.value);
});
if (!blocked.src) {
script.textContent = blocked.textContent;
}
blocked.parentNode.insertBefore(script, blocked);
blocked.parentNode.removeChild(blocked);
}
/**
* Activate an inert template element by injecting its HTML content,
* then removing the template to prevent double execution.
*
* @param {HTMLTemplateElement} tmpl The template element.
*/
function activateTemplate(tmpl) {
var container = tmpl.parentNode;
injectHTML(tmpl.innerHTML, container);
container.removeChild(tmpl);
}
/**