PluginProbe
seQura / 4.0.0
seQura v4.0.0
4.3.4 4.3.3 4.3.2 4.3.1 trunk 2.0.0 2.0.10 2.0.11 2.0.12 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 3.0.0 3.0.2 3.0.5 3.0.6 3.0.7 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 4.0.0 All 30 releases
sequra / assets / js / src / page / widget-facade.js

widget-facade.js in seQura 4.0.0, at assets/js/src/page/widget-facade.js

367 lines 17.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 isAmountInAllowedRange: function (widget, cents) {
228 if ('undefined' !== typeof widget.minAmount && widget.minAmount && cents < widget.minAmount) {
229 return false;
230 }
231
232 return !(
233 'undefined' !== typeof widget.maxAmount &&
234 widget.maxAmount &&
235 parseInt(widget.maxAmount, 10) !== 0 &&
236 widget.maxAmount < cents
237 );
238 },
239
240 drawMiniWidgetOnElement: function (widget, element, priceElem) {
241 if (!priceElem) {
242 const priceSrc = this.getPriceSelector(widget);
243 priceElem = document.querySelector(priceSrc);
244 if (!priceElem) {
245 console.error(priceSrc + ' is not a valid css selector to read the price from, for seQura mini-widget.');
246 return;
247 }
248 }
249 const cents = this.nodeToCents(priceElem);
250
251 const className = 'sequra-educational-popup';
252 const modifierClassName = className + '--' + widget.product;
253
254 const oldWidget = element.parentNode.querySelector('.' + className + '.' + modifierClassName);
255 if (oldWidget) {
256 if (cents == oldWidget.getAttribute('data-amount')) {
257 return; // no need to update the widget, the price is the same.
258 }
259
260 oldWidget.remove();// remove the old widget to draw a new one.
261 }
262
263 if (!this.isAmountInAllowedRange(widget, cents)) {
264 return;
265 }
266
267 const widgetNode = document.createElement('small');
268 widgetNode.className = `sequra-promotion-miniwidget ${className} ${modifierClassName}`;
269 widgetNode.setAttribute('data-amount', cents);
270 widgetNode.setAttribute('data-product', widget.product);
271
272 const creditAgreements = Sequra.computeCreditAgreements({ amount: cents, product: widget.product })[widget.product];
273 let creditAgreement = null
274 do {
275 creditAgreement = creditAgreements.pop();
276 } while (cents < creditAgreement.min_amount.value && creditAgreements.length > 1);
277 if (cents < creditAgreement.min_amount.value && !widget.messageBelowLimit) {
278 return;
279 }
280
281 if (cents >= creditAgreement.min_amount.value) {
282 widgetNode.innerText = widget.message.replace('%s', creditAgreement.instalment_total.string);
283 } else {
284 if (!widget.messageBelowLimit) {
285 return;
286 }
287 widgetNode.innerText = widget.messageBelowLimit.replace('%s', creditAgreement.min_amount.string);
288 }
289
290 if (element.nextSibling) {//Insert after
291 element.parentNode.insertBefore(widgetNode, element.nextSibling);
292 } else {
293 element.parentNode.appendChild(widgetNode);
294 }
295 this.refreshComponents();
296 },
297
298 drawWidgetOnElement: function (widget, element) {
299 const priceSrc = this.getPriceSelector(widget);
300 const priceElem = document.querySelector(priceSrc);
301 if (!priceElem) {
302 console.error(priceSrc + ' is not a valid css selector to read the price from, for seQura widget.');
303 return;
304 }
305 const cents = this.nodeToCents(priceElem);
306
307 const className = 'sequra-promotion-widget';
308 const modifierClassName = className + '--' + widget.product;
309
310 const oldWidget = element.parentNode.querySelector('.' + className + '.' + modifierClassName);
311 if (oldWidget) {
312 if (cents == oldWidget.getAttribute('data-amount')) {
313 return; // no need to update the widget, the price is the same.
314 }
315
316 oldWidget.remove();// remove the old widget to draw a new one.
317 }
318
319 if (!this.isAmountInAllowedRange(widget, cents)) {
320 return;
321 }
322
323 const promoWidgetNode = document.createElement('div');
324 promoWidgetNode.className = className + ' ' + modifierClassName;
325 promoWidgetNode.setAttribute('data-amount', cents);
326 promoWidgetNode.setAttribute('data-product', widget.product);
327
328 const theme = this.presets[widget.theme] ? this.presets[widget.theme] : widget.theme;
329 try {
330 const attributes = JSON.parse(theme);
331 for (let key in attributes) {
332 promoWidgetNode.setAttribute('data-' + key, "" + attributes[key]);
333 }
334 } catch (e) {
335 promoWidgetNode.setAttribute('data-type', 'text');
336 }
337
338 if (widget.campaign) {
339 promoWidgetNode.setAttribute('data-campaign', widget.campaign);
340 }
341 if (widget.registrationAmount) {
342 promoWidgetNode.setAttribute('data-registration-amount', widget.registrationAmount);
343 }
344
345 if (element.nextSibling) {//Insert after
346 element.parentNode.insertBefore(promoWidgetNode, element.nextSibling);
347 } else {
348 element.parentNode.appendChild(promoWidgetNode);
349 }
350 this.refreshComponents();
351 }
352 }
353 };
354
355 SequraWidgetFacade.init()
356 Sequra.onLoad(() => {
357 SequraWidgetFacade.drawWidgetsOnPage();
358 if ('undefined' !== typeof jQuery) {
359 const variationForm = jQuery('.variations_form');
360 if (variationForm.length) {
361 variationForm.on('show_variation', () => SequraWidgetFacade.drawWidgetsOnPage(false));
362 variationForm.on('hide_variation', () => SequraWidgetFacade.drawWidgetsOnPage());
363 }
364 }
365 });
366 });
367 })();