| 1 |
(function () { |
| 2 |
document.addEventListener('DOMContentLoaded', () => { |
| 3 |
if (!SequraConfigParams || !SequraWidgetFacade) { |
| 4 |
return; |
| 5 |
} |
| 6 |
|
| 7 |
SequraWidgetFacade = { |
| 8 |
...{ |
| 9 |
widgets: [], |
| 10 |
miniWidgets: [], |
| 11 |
}, |
| 12 |
...SequraConfigParams, |
| 13 |
...SequraWidgetFacade, |
| 14 |
...{ |
| 15 |
mutationObserver: null, |
| 16 |
forcePriceSelector: true, |
| 17 |
presets: { |
| 18 |
L: '{"alignment":"left"}', |
| 19 |
R: '{"alignment":"right"}', |
| 20 |
legacy: '{"type":"legacy"}', |
| 21 |
legacyL: '{"type":"legacy","alignment":"left"}', |
| 22 |
legacyR: '{"type":"legacy","alignment":"right"}', |
| 23 |
minimal: '{"type":"text","branding":"none","size":"S","starting-text":"as-low-as"}', |
| 24 |
minimalL: '{"type":"text","branding":"none","size":"S","starting-text":"as-low-as","alignment":"left"}', |
| 25 |
minimalR: '{"type":"text","branding":"none","size":"S","starting-text":"as-low-as","alignment":"right"}' |
| 26 |
}, |
| 27 |
|
| 28 |
init: function () { |
| 29 |
// Remove duplicated objects from this.widgets. |
| 30 |
const uniqueWidgets = []; |
| 31 |
this.widgets.forEach(widget => { |
| 32 |
Object.keys(widget).forEach(key => { |
| 33 |
if (typeof widget[key] === 'string') { |
| 34 |
widget[key] = this.decodeEntities(widget[key]); |
| 35 |
} |
| 36 |
}); |
| 37 |
|
| 38 |
if (!uniqueWidgets.some(w => w.price_src === widget.price_src && w.dest === widget.dest && w.product === widget.product && w.theme === widget.theme && w.reverse === widget.reverse && w.campaign === widget.campaign)) { |
| 39 |
uniqueWidgets.push(widget); |
| 40 |
} |
| 41 |
}); |
| 42 |
this.widgets = uniqueWidgets; |
| 43 |
}, |
| 44 |
|
| 45 |
getText: function (selector) { |
| 46 |
return selector && document.querySelector(selector) ? document.querySelector(selector).textContent : "0"; |
| 47 |
}, |
| 48 |
|
| 49 |
nodeToCents: function (node) { |
| 50 |
return this.textToCents(node ? node.textContent : "0"); |
| 51 |
}, |
| 52 |
|
| 53 |
selectorToCents: function (selector) { |
| 54 |
return this.textToCents(this.getText(selector)); |
| 55 |
}, |
| 56 |
|
| 57 |
decodeEntities: function (encodedString) { |
| 58 |
if (!encodedString.match(/&(nbsp|amp|quot|lt|gt|#\d+|#x[0-9A-Fa-f]+);/g)) { |
| 59 |
return encodedString; |
| 60 |
} |
| 61 |
const elem = document.createElement('div'); |
| 62 |
elem.innerHTML = encodedString; |
| 63 |
return elem.textContent; |
| 64 |
}, |
| 65 |
|
| 66 |
textToCents: function (text) { |
| 67 |
const thousandSeparator = this.decodeEntities(this.thousandSeparator); |
| 68 |
const decimalSeparator = this.decodeEntities(this.decimalSeparator); |
| 69 |
|
| 70 |
text = text.replace(/^\D*/, '').replace(/\D*$/, ''); |
| 71 |
if (text.indexOf(decimalSeparator) < 0) { |
| 72 |
text += decimalSeparator + '00'; |
| 73 |
} |
| 74 |
return this.floatToCents( |
| 75 |
parseFloat( |
| 76 |
text |
| 77 |
.replace(thousandSeparator, '') |
| 78 |
.replace(decimalSeparator, '.') |
| 79 |
) |
| 80 |
); |
| 81 |
}, |
| 82 |
|
| 83 |
floatToCents: function (value) { |
| 84 |
return parseInt(value.toFixed(2).replace('.', ''), 10); |
| 85 |
}, |
| 86 |
|
| 87 |
refreshComponents: function () { |
| 88 |
Sequra.onLoad( |
| 89 |
function () { |
| 90 |
Sequra.refreshComponents(); |
| 91 |
} |
| 92 |
); |
| 93 |
}, |
| 94 |
|
| 95 |
isVariableProduct: function (selector) { |
| 96 |
return document.querySelector(selector) ? true : false; |
| 97 |
}, |
| 98 |
|
| 99 |
getPriceSelector: function (widget) { |
| 100 |
return !this.forcePriceSelector && this.isVariableProduct(widget.isVariableSel) ? widget.variationPriceSel : widget.priceSel; |
| 101 |
}, |
| 102 |
|
| 103 |
/** |
| 104 |
* Search for child elements in the parentElem that are targets of the widget |
| 105 |
* @param {object} parentElem DOM element that may contains the widget's targets |
| 106 |
* @param {object} widget Widget object |
| 107 |
* @param {string} observedAt Unique identifier to avoid fetch the same element multiple times |
| 108 |
* @returns {array} Array of objects containing the target elements and a reference to the widget |
| 109 |
*/ |
| 110 |
getWidgetTargets: function (parentElem, widget, observedAt) { |
| 111 |
const targets = []; |
| 112 |
if (widget.dest) { |
| 113 |
const children = parentElem.querySelectorAll(widget.dest); |
| 114 |
const productObservedAttr = 'data-sequra-observed-' + widget.product; |
| 115 |
for (const child of children) { |
| 116 |
if (child.getAttribute(productObservedAttr) == observedAt) { |
| 117 |
continue;// skip elements that are already observed in this mutation. |
| 118 |
} |
| 119 |
child.setAttribute(productObservedAttr, observedAt); |
| 120 |
targets.push({ elem: child, widget }); |
| 121 |
} |
| 122 |
} |
| 123 |
return targets; |
| 124 |
}, |
| 125 |
|
| 126 |
/** |
| 127 |
* Search for child elements in the parentElem that are targets of the widget |
| 128 |
* @param {object} widget Widget object |
| 129 |
* @returns {array} Array of objects containing the target elements and a reference to the widget |
| 130 |
*/ |
| 131 |
getMiniWidgetTargets: function (widget) { |
| 132 |
const targets = []; |
| 133 |
if (widget.dest) { |
| 134 |
const children = document.querySelectorAll(widget.dest); |
| 135 |
const prices = document.querySelectorAll(widget.priceSel); |
| 136 |
const priceObservedAttr = 'data-sequra-observed-price-' + widget.product; |
| 137 |
|
| 138 |
for (let i = 0; i < children.length; i++) { |
| 139 |
const child = children[i]; |
| 140 |
|
| 141 |
const priceElem = 'undefined' !== typeof prices[i] ? prices[i] : null; |
| 142 |
const priceValue = priceElem ? this.nodeToCents(priceElem) : null; |
| 143 |
|
| 144 |
if (null === priceValue || child.getAttribute(priceObservedAttr) == priceValue) { |
| 145 |
continue; |
| 146 |
} |
| 147 |
child.setAttribute(priceObservedAttr, priceValue); |
| 148 |
targets.push({ elem: child, priceElem, widget }); |
| 149 |
} |
| 150 |
} |
| 151 |
return targets; |
| 152 |
}, |
| 153 |
|
| 154 |
/** |
| 155 |
* Get an unique identifier to avoid fetch the same element multiple times |
| 156 |
* @returns {number} The current timestamp |
| 157 |
*/ |
| 158 |
getObservedAt: () => Date.now(), |
| 159 |
|
| 160 |
removeWidgetsOnPage: function () { |
| 161 |
if (this.mutationObserver) { |
| 162 |
this.mutationObserver.disconnect(); |
| 163 |
} |
| 164 |
document.querySelectorAll('.sequra-promotion-widget').forEach(widget => widget.remove()); |
| 165 |
if (this.mutationObserver) { |
| 166 |
this.mutationObserver.observe(document, { childList: true, subtree: true }); |
| 167 |
} |
| 168 |
}, |
| 169 |
|
| 170 |
/** |
| 171 |
* Draw the missing or outdated widgets in the page. |
| 172 |
*/ |
| 173 |
refreshWidgets: function () { |
| 174 |
|
| 175 |
const targets = []; |
| 176 |
for (const widget of this.widgets) { |
| 177 |
const widgetTargets = this.getWidgetTargets(document, widget, this.getObservedAt()); |
| 178 |
targets.push(...widgetTargets); |
| 179 |
} |
| 180 |
for (const miniWidget of this.miniWidgets) { |
| 181 |
const widgetTargets = this.getMiniWidgetTargets(miniWidget); |
| 182 |
targets.push(...widgetTargets); |
| 183 |
} |
| 184 |
|
| 185 |
targets.forEach(target => { |
| 186 |
const { elem, widget } = target; |
| 187 |
this.isMiniWidget(widget) ? this.drawMiniWidgetOnElement(widget, elem, target.priceElem) : this.drawWidgetOnElement(widget, elem); |
| 188 |
}); |
| 189 |
}, |
| 190 |
|
| 191 |
/** |
| 192 |
* Paint the widgets in the page and observe the DOM to refresh the widgets when the page changes. |
| 193 |
* @param {boolean} forcePriceSelector If true, the price selector will be forced to the simple product price selector. |
| 194 |
*/ |
| 195 |
drawWidgetsOnPage: function (forcePriceSelector = true) { |
| 196 |
if (!this.widgets.length && !this.miniWidgets.length) { |
| 197 |
return; |
| 198 |
} |
| 199 |
|
| 200 |
if (this.mutationObserver) { |
| 201 |
this.mutationObserver.disconnect(); |
| 202 |
} |
| 203 |
|
| 204 |
this.forcePriceSelector = forcePriceSelector; |
| 205 |
|
| 206 |
this.refreshWidgets(); |
| 207 |
|
| 208 |
// Then, observe the DOM to refresh the widgets when the page changes. |
| 209 |
this.mutationObserver = new MutationObserver((mutations) => { |
| 210 |
this.mutationObserver.disconnect();// disable the observer to avoid multiple calls to the same function. |
| 211 |
for (const mutation of mutations) { |
| 212 |
if (['childList', 'subtree', 'characterData'].includes(mutation.type)) { |
| 213 |
this.refreshWidgets(); |
| 214 |
break; |
| 215 |
} |
| 216 |
} |
| 217 |
this.mutationObserver.observe(document, { childList: true, subtree: true, characterData: true }); // enable the observer again. |
| 218 |
}); |
| 219 |
|
| 220 |
this.mutationObserver.observe(document, { childList: true, subtree: true, characterData: true }); |
| 221 |
}, |
| 222 |
|
| 223 |
isMiniWidget: function (widget) { |
| 224 |
return this.miniWidgets.indexOf(widget) !== -1; |
| 225 |
}, |
| 226 |
|
| 227 |
drawMiniWidgetOnElement: function (widget, element, priceElem) { |
| 228 |
if (!priceElem) { |
| 229 |
const priceSrc = this.getPriceSelector(widget); |
| 230 |
priceElem = document.querySelector(priceSrc); |
| 231 |
if (!priceElem) { |
| 232 |
console.error(priceSrc + ' is not a valid css selector to read the price from, for seQura mini-widget.'); |
| 233 |
return; |
| 234 |
} |
| 235 |
} |
| 236 |
const cents = this.nodeToCents(priceElem); |
| 237 |
|
| 238 |
|
| 239 |
|
| 240 |
const className = 'sequra-educational-popup'; |
| 241 |
const modifierClassName = className + '--' + widget.product; |
| 242 |
|
| 243 |
const oldWidget = element.parentNode.querySelector('.' + className + '.' + modifierClassName); |
| 244 |
if (oldWidget) { |
| 245 |
if (cents == oldWidget.getAttribute('data-amount')) { |
| 246 |
return; // no need to update the widget, the price is the same. |
| 247 |
} |
| 248 |
|
| 249 |
oldWidget.remove();// remove the old widget to draw a new one. |
| 250 |
} |
| 251 |
|
| 252 |
if (widget.maxAmount && widget.maxAmount < cents) { |
| 253 |
return; |
| 254 |
} |
| 255 |
|
| 256 |
const widgetNode = document.createElement('small'); |
| 257 |
widgetNode.className = className + ' ' + modifierClassName; |
| 258 |
widgetNode.setAttribute('data-amount', cents); |
| 259 |
widgetNode.setAttribute('data-product', widget.product); |
| 260 |
|
| 261 |
const creditAgreements = Sequra.computeCreditAgreements({ amount: cents, product: widget.product })[widget.product]; |
| 262 |
let creditAgreement = null |
| 263 |
do { |
| 264 |
creditAgreement = creditAgreements.pop(); |
| 265 |
} while (cents < creditAgreement.min_amount.value && creditAgreements.length > 1); |
| 266 |
if (cents < creditAgreement.min_amount.value && !widget.messageBelowLimit) { |
| 267 |
return; |
| 268 |
} |
| 269 |
|
| 270 |
if (cents >= creditAgreement.min_amount.value) { |
| 271 |
widgetNode.innerText = widget.message.replace('%s', creditAgreement.instalment_total.string); |
| 272 |
} else { |
| 273 |
if (!widget.messageBelowLimit) { |
| 274 |
return; |
| 275 |
} |
| 276 |
widgetNode.innerText = widget.messageBelowLimit.replace('%s', creditAgreement.min_amount.string); |
| 277 |
} |
| 278 |
|
| 279 |
if (element.nextSibling) {//Insert after |
| 280 |
element.parentNode.insertBefore(widgetNode, element.nextSibling); |
| 281 |
this.refreshComponents(); |
| 282 |
} else { |
| 283 |
element.parentNode.appendChild(widgetNode); |
| 284 |
} |
| 285 |
|
| 286 |
}, |
| 287 |
|
| 288 |
drawWidgetOnElement: function (widget, element) { |
| 289 |
const priceSrc = this.getPriceSelector(widget); |
| 290 |
const priceElem = document.querySelector(priceSrc); |
| 291 |
if (!priceElem) { |
| 292 |
console.error(priceSrc + ' is not a valid css selector to read the price from, for seQura widget.'); |
| 293 |
return; |
| 294 |
} |
| 295 |
const cents = this.nodeToCents(priceElem); |
| 296 |
|
| 297 |
const className = 'sequra-promotion-widget'; |
| 298 |
const modifierClassName = className + '--' + widget.product; |
| 299 |
|
| 300 |
const oldWidget = element.parentNode.querySelector('.' + className + '.' + modifierClassName); |
| 301 |
if (oldWidget) { |
| 302 |
if (cents == oldWidget.getAttribute('data-amount')) { |
| 303 |
return; // no need to update the widget, the price is the same. |
| 304 |
} |
| 305 |
|
| 306 |
oldWidget.remove();// remove the old widget to draw a new one. |
| 307 |
} |
| 308 |
|
| 309 |
const promoWidgetNode = document.createElement('div'); |
| 310 |
promoWidgetNode.className = className + ' ' + modifierClassName; |
| 311 |
promoWidgetNode.setAttribute('data-amount', cents); |
| 312 |
promoWidgetNode.setAttribute('data-product', widget.product); |
| 313 |
|
| 314 |
const theme = this.presets[widget.theme] ? this.presets[widget.theme] : widget.theme; |
| 315 |
try { |
| 316 |
const attributes = JSON.parse(theme); |
| 317 |
for (let key in attributes) { |
| 318 |
promoWidgetNode.setAttribute('data-' + key, "" + attributes[key]); |
| 319 |
} |
| 320 |
} catch (e) { |
| 321 |
promoWidgetNode.setAttribute('data-type', 'text'); |
| 322 |
} |
| 323 |
|
| 324 |
if (widget.campaign) { |
| 325 |
promoWidgetNode.setAttribute('data-campaign', widget.campaign); |
| 326 |
} |
| 327 |
if (widget.registrationAmount) { |
| 328 |
promoWidgetNode.setAttribute('data-registration-amount', widget.registrationAmount); |
| 329 |
} |
| 330 |
|
| 331 |
if (element.nextSibling) {//Insert after |
| 332 |
element.parentNode.insertBefore(promoWidgetNode, element.nextSibling); |
| 333 |
this.refreshComponents(); |
| 334 |
} else { |
| 335 |
element.parentNode.appendChild(promoWidgetNode); |
| 336 |
} |
| 337 |
} |
| 338 |
} |
| 339 |
}; |
| 340 |
|
| 341 |
SequraWidgetFacade.init() |
| 342 |
Sequra.onLoad(() => { |
| 343 |
SequraWidgetFacade.drawWidgetsOnPage(); |
| 344 |
if ('undefined' !== typeof jQuery) { |
| 345 |
const variationForm = jQuery('.variations_form'); |
| 346 |
if (variationForm.length) { |
| 347 |
variationForm.on('show_variation', () => SequraWidgetFacade.drawWidgetsOnPage(false)); |
| 348 |
variationForm.on('hide_variation', () => SequraWidgetFacade.drawWidgetsOnPage()); |
| 349 |
} |
| 350 |
} |
| 351 |
}); |
| 352 |
}); |
| 353 |
})(); |