PluginProbe
seQura / 3.2.2
seQura v3.2.2
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 3.2.2, at assets/js/src/page/widget-facade.js

353 lines 16.5 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 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 })();