Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
Binary file added dist/.DS_Store
Binary file not shown.
2 changes: 1 addition & 1 deletion dist/cm-no-css.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/cm.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/ide-no-css.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/ide.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/ide.min.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/klaro-no-css.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/klaro-no-translations-no-css.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/klaro-no-translations.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/klaro-no-translations.min.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/klaro.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/klaro.min.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/translations-no-css.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/translations.js

Large diffs are not rendered by default.

222 changes: 189 additions & 33 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/components/contextual-consent-notice.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ const ContextualConsentNotice = ({manager, style, config, t, lang, service}) =>
const decline = () => {}
const accept = () => {
manager.updateConsent(service.name, true)
manager.temporaryConsents[service.name] = true
if (manager.confirmed){ // we permanently save the consent state
manager.saveConsents('contextual-accept')
manager.applyConsents(false, true, service.name)
delete manager.temporaryConsents[service.name] // clear temporary flag after permanent save
} else // we only temporarily accept this
manager.applyConsents(false, true, service.name)
}
const acceptOnce = () => {
manager.updateConsent(service.name, true)
manager.temporaryConsents[service.name] = true
manager.applyConsents(false, true, service.name)
manager.updateConsent(service.name, false)
}
const handleShowModal = (e) => {
e.preventDefault()
Expand Down
99 changes: 90 additions & 9 deletions src/consent-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default class ConsentManager {
this.states = {} // keep track of the change (enabled, disabled) of individual services
this.initialized = {} // keep track of which services have been initialized already
this.executedOnce = {} //keep track of which services have been executed at least once
this.temporaryConsents = {} // keep track of temporary/interactive consents (acceptOnce)
this.watchers = new Set([])
this.loadConsents()
this.applyConsents()
Expand Down Expand Up @@ -96,7 +97,10 @@ export default class ConsentManager {

changeAll(value){
let changedServices = 0
this.config.services.filter(service => !service.contextualConsentOnly).map(service => {
const servicesToProcess = this.config.services.filter(service => !service.contextualConsentOnly)

// First pass: update all services
servicesToProcess.map(service => {
if(service.required || this.config.required || value) {
if (this.updateConsent(service.name, true))
changedServices++
Expand All @@ -105,12 +109,52 @@ export default class ConsentManager {
changedServices++
}
})

// Second pass: ensure dependencies are enabled for enabled services
if (value) {
servicesToProcess.forEach(service => {
if (this.consents[service.name] && service.dependsOn) {
const dependencies = Array.isArray(service.dependsOn) ? service.dependsOn : [service.dependsOn]
dependencies.forEach(depName => {
if (!this.consents[depName]) {
this.consents[depName] = true
changedServices++
}
})
}
})
}

return changedServices
}

updateConsent(name, value){
const changed = (this.consents[name] || false) !== value
this.consents[name] = value

// Handle dependsOn: if enabling a service, also enable its dependencies
if (value) {
const service = this.getService(name)
if (service && service.dependsOn) {
const dependencies = Array.isArray(service.dependsOn) ? service.dependsOn : [service.dependsOn]
dependencies.forEach(depName => {
if (!this.consents[depName]) {
this.consents[depName] = true
}
})
}
} else {
// If disabling a service, also disable all services that depend on it
this.config.services.forEach(service => {
if (service.dependsOn) {
const dependencies = Array.isArray(service.dependsOn) ? service.dependsOn : [service.dependsOn]
if (dependencies.includes(name) && this.consents[service.name]) {
this.consents[service.name] = false
}
}
})
}

this.notify('consents', this.consents)
return changed
}
Expand All @@ -119,6 +163,7 @@ export default class ConsentManager {
this.consents = this.defaultConsents
this.states = {}
this.confirmed = false
this.temporaryConsents = {}
this.applyConsents()
this.savedConsents = {...this.consents}
this.store.delete()
Expand Down Expand Up @@ -158,6 +203,7 @@ export default class ConsentManager {
this.store.set(v);
this.confirmed = true
this.changed = false
this.temporaryConsents = {}
const changes = this.changedConsents()
this.savedConsents = {...this.consents}
this.notify('saveConsents', {changes: changes, consents: this.consents, type: eventType || 'script'})
Expand Down Expand Up @@ -203,7 +249,7 @@ export default class ConsentManager {
const optOut = (service.optOut !== undefined ? service.optOut : (this.config.optOut || false))
const required = (service.required !== undefined ? service.required : (this.config.required || false))
//opt out and required services are always treated as confirmed
const confirmed = this.confirmed || optOut || dryRun || interactive
const confirmed = this.confirmed || optOut || dryRun || interactive || this.temporaryConsents[service.name]
const consent = (this.getConsent(service.name) && confirmed) || required
const handlerOpts = {service: service, config: this.config, vars: vars, consents: this.consents, confirmed: this.confirmed}

Expand Down Expand Up @@ -304,11 +350,31 @@ export default class ConsentManager {
parent.insertBefore(newElement, element)
parent.removeChild(element)
} else if (element.tagName === 'SCRIPT' || element.tagName === 'LINK'){
// this element is already active, we do not touch it...
if (consent && element.type === (type || "") && element.src === src){
// eslint-disable-next-line no-console
console.debug(`Skipping ${element.tagName} for service ${service.name}, as it already has the correct type or src...`)
continue
// this element is already in the correct state, we do not touch it...
// Use getAttribute to avoid browser normalization issues
const actualType = element.getAttribute('type')
const actualSrc = element.getAttribute('src')
const actualHref = element.getAttribute('href')

if (consent){
// check if script is already active with correct type and src
const expectedType = type || ""
const hasCorrectType = actualType === expectedType
const hasCorrectSrc = (src === undefined && !actualSrc) || actualSrc === src
const hasCorrectHref = (href === undefined && !actualHref) || actualHref === href

if (hasCorrectType && hasCorrectSrc && hasCorrectHref){
// eslint-disable-next-line no-console
console.debug(`Skipping ${element.tagName} for service ${service.name}, as it already has the correct type or src...`)
continue
}
} else {
// check if script is already blocked
if (actualType === 'text/plain'){
// eslint-disable-next-line no-console
console.debug(`Skipping ${element.tagName} for service ${service.name}, as it is already blocked...`)
continue
}
}
// we create a new script instead of updating the node in
// place, as the script won't start correctly otherwise
Expand All @@ -317,6 +383,9 @@ export default class ConsentManager {
newElement.setAttribute(attribute.name, attribute.value)
}

// Mark this element as internal to prevent MutationObserver loops
newElement.setAttribute('data-klaro-internal', 'true')

if (element.hasAttribute('nonce')) {
newElement.setAttribute('nonce', element.nonce)
}
Expand All @@ -337,6 +406,18 @@ export default class ConsentManager {
parent.removeChild(element)
} else {
// all other elements (images etc.) are modified in place...
// Check if element is already in the correct state (similar to script handling)
const currentDisplay = element.style.display
const isCurrentlyHidden = currentDisplay === 'none'
const shouldBeHidden = !consent

if (isCurrentlyHidden === shouldBeHidden && ds['modified-by-klaro'] === 'yes'){
// Element is already in the correct state, skip modification
// eslint-disable-next-line no-console
console.debug(`Skipping ${element.tagName} for service ${service.name}, as it is already in the correct state...`)
continue
}

if (consent){
for(const attr of attrs){
const attrValue = ds[attr]
Expand All @@ -348,7 +429,7 @@ export default class ConsentManager {
}
if (ds.title !== undefined)
element.title = ds.title
if (ds['original-display'] !== undefined){
if (ds['original-display'] !== undefined && ds['original-display'] !== ''){
element.style.display = ds['original-display']
} else {
element.style.removeProperty('display')
Expand All @@ -358,7 +439,7 @@ export default class ConsentManager {
if (ds.title !== undefined)
element.removeAttribute('title')
if (ds['original-display'] === undefined && element.style.display !== undefined)
ds['original-display'] = element.style.display
ds['original-display'] = element.style.display || ''
element.style.display = 'none'
for(const attr of attrs){
const attrValue = ds[attr]
Expand Down
39 changes: 39 additions & 0 deletions src/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,45 @@ export function setup(config){
if (hashParams.has('klaro-ide')){
showKlaroIDE(script)
}

if (defaultConfig && defaultConfig.watch){
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false
for(const mutation of mutations){
for(const node of mutation.addedNodes){
if (node.nodeType !== 1) continue
// Ignore elements created by Klaro itself
if (node.hasAttribute('data-klaro-internal')) continue
if (node.hasAttribute('data-name')){
shouldUpdate = true
break
}
if (node.querySelector('[data-name]')){
shouldUpdate = true
break
}
}
if (shouldUpdate) break
}

if (shouldUpdate){
const manager = getManager(defaultConfig)
manager.applyConsents()
}

// we check if the Klaro element is still there
const element = document.getElementById(getElementID(defaultConfig))
if (element === null){
initialize({})
}
})
if (document.body)
observer.observe(document.body, {childList: true, subtree: true})
else
document.addEventListener("DOMContentLoaded", () => {
observer.observe(document.body, {childList: true, subtree: true})
})
}
}

export function show(config, modal, api){
Expand Down
2 changes: 1 addition & 1 deletion src/translations/hu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@ contextualConsent:
description: Szeretné betölteni azt a külső tartalmat, amelyet {title} szolgáltat?
acceptOnce: Igen
acceptAlways: Mindig
descriptionEmptyStore: Amennyiben beleegyezik ebbe a szolgáltatásba, el kell fogadnia a következőket: {title} itt: {link}.
descriptionEmptyStore: 'Amennyiben beleegyezik ebbe a szolgáltatásba, el kell fogadnia a következőket: {title} itt: {link}.'
modalLinkText: Jóváhagyás kezelő