PageSourceSearch

https://residusofficial.com/wp-content/plugins/gtm-server-side/assets/js/javascript.js?ver=2.3.10

js residusofficial.com collected 2026-10-08 06:29:04 UTC 16,620 bytes, 667 lines download raw bytes

1/**
2 * Front-end js file.
3 *
4 * @package GTM_Server_Side
5 */
6
7jQuery( document ).ready(
8	function () {
9		jQuery( document ).on(
10			'click',
11			'body.woocommerce-cart [name=update_cart]',
12			function () {
13				pluginGtmServerSide.changeCartQty();
14			}
15		);
16
17		jQuery( document ).on(
18			'keypress',
19			'body.woocommerce-cart .woocommerce-cart-form input[type=number]',
20			function () {
21				pluginGtmServerSide.changeCartQty();
22			}
23		);
24
25		jQuery( document ).on(
26			'click',
27			'.add_to_cart_button:not(.product_type_variable, .product_type_grouped, .single_add_to_cart_button)',
28			function ( e ) {
29				var el = e.currentTarget || e.target.closest( '.add_to_cart_button' );
30				if ( ! el || ! el.dataset ) {
31					return;
32				}
33
34				if ( ! el.dataset.gtm_item_id ) {
35					return;
36				}
37
38				let gtmData    = pluginGtmServerSide.getGtmItemData( el.dataset );
39				let customData = pluginGtmServerSide.getCustomItemData( el.dataset );
40
41				pluginGtmServerSide.trackAddToCart( gtmData, this );
42				pluginGtmServerSide.pushSelectItem( gtmData, customData );
43			}
44		);
45
46		jQuery( document ).on(
47			'click',
48			'.wc-block-grid__product .add_to_cart_button:not(.product_type_variable, .product_type_grouped, .single_add_to_cart_button)',
49			function ( e ) {
50				var $el = jQuery( this ).closest( '.wc-block-grid__product' );
51				if ( ! $el.length ) {
52					return;
53				}
54
55				if ( ! $el.data( 'gtm_item_id' ) ) {
56					return;
57				}
58
59				let gtmData    = pluginGtmServerSide.getGtmItemData( $el.data() );
60				let customData = pluginGtmServerSide.getCustomItemData( $el.data() );
61
62				pluginGtmServerSide.trackAddToCart( gtmData, this );
63				pluginGtmServerSide.pushSelectItem( gtmData, customData );
64			}
65		);
66
67		jQuery( document ).on(
68			'click',
69			'.single_add_to_cart_button:not(.disabled)',
70			function ( e ) {
71				// The "Add to Cart with Options" block's form has the `cart` class
72				// only in its legacy (page-load) mode; otherwise it adds through
73				// the Store API.
74				var $elForm = jQuery( this ).closest( 'form.cart, form.wc-block-add-to-cart-with-options' );
75				if ( ! $elForm.length ) {
76					return true;
77				}
78
79				if ( $elForm.find( '[name=variation_id]' ).length > 0 ) {
80					pluginGtmServerSide.pushVariationProduct( $elForm, this );
81					return;
82				}
83
84				if ( $elForm.hasClass( 'grouped_form' ) || $elForm.find( '[name^=quantity\\[]' ).length > 0 ) {
85					pluginGtmServerSide.pushGroupProduct( $elForm, this );
86					return;
87				}
88
89				pluginGtmServerSide.pushSimpleProduct( $elForm, this );
90			}
91		);
92
93		/**
94		 * Add over AJAX without WooCommerce's own handler: a theme or plugin that
95		 * AJAX-ifies an add-to-cart link or form.cart posts it and then triggers
96		 * added_to_cart to refresh the fragments. The click left the item pending;
97		 * this is where it is pushed, because no page render follows to drain the
98		 * server-side stash.
99		 */
100		jQuery( document.body ).on(
101			'added_to_cart',
102			function () {
103				pluginGtmServerSide.flushPendingAddToCart();
104			}
105		);
106
107		/**
108		 * Delete from minicart
109		 */
110		jQuery( document ).on(
111			'removed_from_cart',
112			function ( e, fragments, cart_hash, $thisbutton ) {
113				if ( ! $thisbutton.length ) {
114					return;
115				}
116
117				if ( ! $thisbutton.data( 'gtm_item_id' ) ) {
118					return;
119				}
120
121				pluginGtmServerSide.removeFromCart(
122					pluginGtmServerSide.getGtmItemData( $thisbutton.data() )
123				);
124			}
125		);
126
127		/**
128		 * Delete from page: /cart
129		 */
130		jQuery( document ).on(
131			'click',
132			'.woocommerce-cart-form .product-remove > a',
133			function ( e ) {
134				var $el = jQuery( e.currentTarget );
135
136				if ( ! $el.data( 'gtm_item_id' ) ) {
137					return;
138				}
139
140				pluginGtmServerSide.removeFromCart(
141					pluginGtmServerSide.getGtmItemData( $el.data() )
142				);
143			}
144		);
145	}
146);
147
148var pluginGtmServerSide = {
149	/**
150	 * Add built at click time that has not been pushed yet, with the moment it
151	 * was built. See trackAddToCart().
152	 */
153	pendingAddToCart: null,
154
155	/**
156	 * How long a pending add stays eligible for its added_to_cart, in ms.
157	 *
158	 * A click that neither loads a page nor completes an AJAX add (a failed
159	 * validation, an aborted request) would otherwise leave its item behind and
160	 * see it pushed by an unrelated add later on.
161	 */
162	PENDING_ADD_TO_CART_MAX_AGE: 30000,
163
164	/**
165	 * Push an add_to_cart for a click, or hold it until the add lands.
166	 *
167	 * The item is held only when the click is expected to load a page: there
168	 * the click-time push is lost and GTM_Server_Side_Event_AddToCart emits the
169	 * event on the next render instead, and a pending item dies with the page.
170	 * If a theme or plugin AJAX-ifies that link or form, added_to_cart pushes
171	 * the held item. Every other add — WooCommerce's own AJAX handler, or a
172	 * WooCommerce Blocks button adding through the Store API, which triggers no
173	 * added_to_cart — is pushed at click time; the server stashes nothing for
174	 * an AJAX or Store API add, so the push is the only one.
175	 *
176	 * @param object|array item Item or list of items.
177	 * @param element el Clicked element.
178	 */
179	trackAddToCart: function ( item, el ) {
180		if ( this.isAjaxAddToCart( el ) || ! this.isPageLoadAddToCart( el ) ) {
181			this.pendingAddToCart = null;
182			this.pushAddToCart( item );
183
184			return;
185		}
186
187		this.pendingAddToCart = {
188			item: item,
189			time: Date.now(),
190		};
191	},
192
193	/**
194	 * Push the add held by trackAddToCart(), if there is still one to push.
195	 */
196	flushPendingAddToCart: function () {
197		var pending           = this.pendingAddToCart;
198		this.pendingAddToCart = null;
199
200		if ( ! pending ) {
201			return;
202		}
203
204		if ( ( Date.now() - pending.time ) > this.PENDING_ADD_TO_CART_MAX_AGE ) {
205			return;
206		}
207
208		this.pushAddToCart( pending.item );
209	},
210
211	/**
212	 * Whether the clicked control adds the product over AJAX.
213	 *
214	 * This reproduces the condition WooCommerce's own `wc-add-to-cart.js`
215	 * applies before it takes a click over AJAX, so the answer follows the
216	 * handler that is actually present rather than a class name alone:
217	 *
218	 * - the script is enqueued. "Enable AJAX add to cart buttons on archives"
219	 *   is what decides that, and the script localises `wc_add_to_cart_params`,
220	 *   so the global is present exactly when the handler is;
221	 * - the clicked control itself carries `ajax_add_to_cart` (the class is set
222	 *   from the product's own `ajax_add_to_cart` support, independently of the
223	 *   option, so it is not sufficient on its own);
224	 * - it carries `data-product_id`, which the handler posts.
225	 *
226	 * @param element el Clicked element.
227	 * @returns bool
228	 */
229	isAjaxAddToCart: function ( el ) {
230		if ( 'undefined' === typeof wc_add_to_cart_params ) {
231			return false;
232		}
233
234		var $el = jQuery( el );
235
236		return $el.is( '.ajax_add_to_cart' ) && !! $el.attr( 'data-product_id' );
237	},
238
239	/**
240	 * Whether the clicked control is expected to add the product by loading a
241	 * page.
242	 *
243	 * That is a submit of the product's `form.cart`, or a link that goes
244	 * somewhere, such as the plain `?add-to-cart=` link on archives with AJAX
245	 * off. A WooCommerce Blocks product button is a `<button>` outside any
246	 * form, so it does not qualify.
247	 *
248	 * @param element el Clicked element.
249	 * @returns bool
250	 */
251	isPageLoadAddToCart: function ( el ) {
252		var $el = jQuery( el );
253
254		if ( $el.closest( 'form.cart' ).length ) {
255			return true;
256		}
257
258		if ( ! $el.is( 'a' ) ) {
259			return false;
260		}
261
262		var href = $el.attr( 'href' );
263
264		if ( ! href || 0 === href.indexOf( '#' ) ) {
265			return false;
266		}
267
268		try {
269			var protocol = new URL( href, window.location.href ).protocol;
270
271			return 'http:' === protocol || 'https:' === protocol;
272		} catch ( e ) {
273			return false;
274		}
275	},
276
277	pushSimpleProduct: function ( $elForm, el ) {
278		var item = this.convertInputsToObject(
279			$elForm.find( '[name^=gtm_]' )
280		);
281		item     = this.getGtmItemData( item );
282
283		var $elQty = $elForm.find( '[name=quantity]' );
284		if ( $elQty.length ) {
285			item.quantity = $elQty.val();
286		}
287
288		this.trackAddToCart( item, el );
289	},
290
291	pushVariationProduct: function ( $elForm, el ) {
292		var item = this.convertInputsToObject(
293			$elForm.find( '[name^=gtm_]' )
294		);
295		item     = this.getGtmItemData( item );
296
297		var $elQty = $elForm.find( '[name=quantity]' );
298		if ( $elQty.length ) {
299			item.quantity = $elQty.val();
300		}
301
302		var variations = [];
303		$elForm.find( '[name^=attribute_] option:selected' ).each(
304			function () {
305				variations.push( jQuery( this ).text() );
306			}
307		);
308
309		if ( variations.length ) {
310			item.item_variant = variations.join( ',' );
311		}
312
313		this.trackAddToCart( item, el );
314	},
315
316	pushGroupProduct: function ( $elForm, el ) {
317		var items = [];
318		$elForm.find( '[name^=quantity\\[]' ).each(
319			function () {
320				var $elQty = jQuery( this );
321				if ( $elQty.is( ':checkbox' ) && ! $elQty.is( ':checked' ) ) {
322					return;
323				}
324
325				if ( ! ( parseFloat( $elQty.val() ) > 0 ) ) {
326					return;
327				}
328
329				var match = /^quantity\[(\d+)\]$/.exec( $elQty.attr( 'name' ) );
330				if ( ! match ) {
331					return;
332				}
333
334				var item = {
335					quantity: $elQty.val(),
336				};
337				$elForm.find( '[name^=gtm_][name$="[' + match[1] + ']"]' ).each(
338					function () {
339						item[ jQuery( this ).data( 'name' ) ] = jQuery( this ).val();
340					}
341				);
342				items.push( item );
343			}
344		);
345		this.trackAddToCart( items, el );
346	},
347
348	/**
349	 * Remove from cart
350	 *
351	 * @param object item
352	 */
353	removeFromCart: function ( item ) {
354		item.quantity = item.quantity || 1;
355		var eventData = {
356			'event': this.getDataLayerEventName( 'remove_from_cart' ),
357			'ecommerce': {
358				'currency': varGtmServerSide.currency,
359				'items': [
360					item,
361				],
362			},
363		};
364
365		if ( varGtmServerSide.user_data ) {
366			eventData.user_data = {};
367			for ( var key in varGtmServerSide.user_data  ) {
368				eventData.user_data[ key ] = varGtmServerSide.user_data[ key ];
369			}
370		}
371
372		if ( 'yes' === varGtmServerSide.is_custom_event_name ) {
373			this._pushWithStateCartData( eventData );
374
375			return;
376		}
377
378		dataLayer.push( { ecommerce: null } );
379		dataLayer.push( eventData );
380	},
381
382	/**
383	 * Change product quantity in cart
384	 */
385	changeCartQty: function () {
386		var $this = this;
387
388		document.querySelectorAll( '.product-quantity input.qty' ).forEach(
389			function ( el ) {
390				var originalValue = el.defaultValue;
391
392				var currentValue = parseInt( el.value );
393				if ( isNaN( currentValue ) ) {
394					currentValue = originalValue;
395				}
396
397				if ( originalValue != currentValue ) {
398					var elCartItem = el.closest( '.cart_item' );
399					var elDataset  = elCartItem && elCartItem.querySelector( '.remove' );
400					if ( ! elDataset ) {
401						return;
402					}
403
404					if ( originalValue < currentValue ) {
405						var item         = $this.getGtmItemData( elDataset.dataset );
406						item['quantity'] = currentValue - originalValue;
407
408						$this.pushAddToCart( item );
409					}
410				}
411			}
412		);
413	},
414
415	/**
416	 * Return gtm custom data.
417	 *
418	 * @param object items List items.
419	 * @returns object
420	 */
421	getGtmItemData: function ( items ) {
422		return this._getItemData( items, 'gtm_' );
423	},
424
425	/**
426	 * Return gtm custom data.
427	 *
428	 * @param object items List items.
429	 * @returns object
430	 */
431	getCustomItemData: function ( items ) {
432		return this._getItemData( items, 'custom_gtm_' );
433	},
434
435	/**
436	 * Return item data.
437	 *
438	 * @param object items List items.
439	 * @returns object
440	 */
441	_getItemData: function ( items, prefix ) {
442		var result = {};
443		for ( var key in items ) {
444			if ( 0 !== key.indexOf( prefix ) ) {
445				continue;
446			}
447
448			var itemKey       = key.replace( prefix, '' )
449			result[ itemKey ] = items[ key ];
450		}
451		return result;
452	},
453
454	/**
455	 * Convert input elements to object.
456	 *
457	 * @param object $els Elements.
458	 * @returns object
459	 */
460	convertInputsToObject( $els ) {
461		var data = {};
462		if ( ! $els.length ) {
463			return data;
464		}
465
466		$els.each(
467			function () {
468				data[ jQuery( this ).attr( 'name' ) ] = jQuery( this ).val();
469			}
470		);
471		return data;
472	},
473
474	/**
475	 * Filter item price.
476	 *
477	 * @param object item List items.
478	 * @returns object
479	 */
480	filterItemPrice: function ( item ) {
481		if ( typeof item.price == 'string' ) {
482			item.price = parseFloat( item.price );
483			if ( isNaN( item.price ) ) {
484				item.price = 0;
485			}
486		} else if ( typeof item.price != 'number' ) {
487			item.price = 0;
488		}
489		item.price = item.price.toFixed( 2 );
490
491		return item;
492	},
493
494	/**
495	 * Push add_to_cart to dataLayer.
496	 *
497	 * @param mixed item List items.
498	 */
499	pushAddToCart: function ( item ) {
500		this._pushToDataLayer( item, 'add_to_cart', 'product' )
501	},
502
503	/**
504	 * Push add_to_cart to dataLayer.
505	 *
506	 * @param object item List items.
507	 * @param object custom Custom data.
508	 */
509	pushSelectItem: function ( item, custom ) {
510		if ( custom?.pagetype ) { // phpcs:ignore WordPress.WhiteSpace.OperatorSpacing.NoSpaceBefore, WordPress.WhiteSpace.OperatorSpacing.NoSpaceAfter
511			this._pushToDataLayer(
512				item,
513				'select_item',
514				custom.pagetype,
515				{
516					collection_id:  custom?.collection_id,  // phpcs:ignore WordPress.WhiteSpace.OperatorSpacing.NoSpaceBefore, WordPress.WhiteSpace.OperatorSpacing.NoSpaceAfter
517					item_list_name: custom?.item_list_name, // phpcs:ignore WordPress.WhiteSpace.OperatorSpacing.NoSpaceBefore, WordPress.WhiteSpace.OperatorSpacing.NoSpaceAfter
518				}
519			)
520		}
521	},
522
523	/**
524	 * Push to dataLayer.
525	 *
526	 * @param object|array originalItem Single item or list of items.
527	 * @param string event Event name.
528	 * @param string pagetype Page type name.
529	 * @param object customEcommerce Ecommerce data.
530	 */
531	_pushToDataLayer: function ( originalItem, event, pagetype, customEcommerce = {} ) {
532		var originalItems = Array.isArray( originalItem ) ? originalItem : [ originalItem ];
533
534		var items = [];
535		var value = 0;
536		var index = 1;
537		for ( var original of originalItems ) {
538			var item_loop      = Object.assign( {}, original );
539			item_loop.index    = index++;
540			item_loop.quantity = item_loop.quantity ? parseInt( item_loop.quantity, 10 ) : 1;
541			item_loop          = this.filterItemPrice( item_loop );
542			value              = parseFloat( value + ( item_loop.price * item_loop.quantity ) );
543			items.push( item_loop );
544		}
545
546		if ( ! items.length ) {
547			return;
548		}
549
550		let eventDataEcommerce = {
551			'currency': varGtmServerSide.currency,
552			'value': value.toFixed( 2 ),
553			'items': items,
554		}
555
556		var eventData = {
557			'event':          this.getDataLayerEventName( event ),
558			'ecomm_pagetype': pagetype,
559			'ecommerce': Object.assign( {}, eventDataEcommerce, customEcommerce ),
560		};
561		if ( varGtmServerSide.user_data ) {
562			eventData.user_data = {};
563			for ( var key in varGtmServerSide.user_data  ) {
564				eventData.user_data[ key ] = varGtmServerSide.user_data[ key ];
565			}
566		}
567
568		if ( 'add_to_cart' === event && 'yes' === varGtmServerSide.is_custom_event_name ) {
569			this._pushWithStateCartData( eventData );
570
571			return;
572		}
573
574		dataLayer.push( { ecommerce: null } );
575		dataLayer.push( eventData );
576	},
577
578	/**
579	 * Return data layer event name.
580	 *
581	 * @param  string $event_name Event name.
582	 * @return string
583	 */
584	getDataLayerEventName: function( event_name ) {
585		if ( 'yes' === varGtmServerSide.is_custom_event_name ) {
586			return event_name + varGtmServerSide.DATA_LAYER_CUSTOM_EVENT_NAME;
587		}
588		return event_name;
589	},
590
591	/**
592	 * Internal helper: actually performs the AJAX request and pushes to the dataLayer.
593	 *
594	 * @param {object} eventData eventData object.
595	 * @return {void}
596	 */
597	_sendStateCartDataAjax: function( eventData ) {
598		jQuery.post(
599			varGtmServerSide.ajax,
600			{
601				action: 'gtm_server_side_state_cart_data',
602				security: varGtmServerSide.security,
603			},
604			function ( response ) {
605				if ( ! response || ! response.success ) {
606					dataLayer.push( { ecommerce: null } );
607					dataLayer.push( eventData );
608
609					return false;
610				}
611
612				eventData['cart_state'] = response.data;
613
614				dataLayer.push( { ecommerce: null } );
615				dataLayer.push( eventData );
616			}
617		);
618	},
619
620	/**
621	 * Push dataLayer with state cart data
622	 *
623	 * @param {object} eventData Event data object.
624	 * @return {void}
625	 */
626	_pushWithStateCartData: function( eventData ) {
627		let self  = this;
628		let fired = false;
629
630		var handler = function( event, xhr, settings ) {
631			if ( fired ) {
632				return;
633			}
634
635			if ( ! settings || ! settings.url ) {
636				return;
637			}
638
639			let url = settings.url;
640
641			if (
642				url.indexOf( 'wc-ajax=add_to_cart' ) !== -1 ||
643				url.indexOf( 'wc-ajax=remove_from_cart' ) !== -1
644			) {
645				fired = true;
646
647				jQuery( document ).off( 'ajaxComplete', handler );
648
649				self._sendStateCartDataAjax( eventData );
650			}
651		};
652
653		jQuery( document ).on( 'ajaxComplete', handler );
654
655		setTimeout(
656			function() {
657				if ( fired ) {
658					return;
659				}
660
661				jQuery( document ).off( 'ajaxComplete', handler );
662				self._sendStateCartDataAjax( eventData );
663			},
664			1500
665		);
666	}
667};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.