PageSourceSearch

https://combsbrothersllc.com/wp-content/themes/enfold/config-templ…/slideshow/phone-track.js?ver=8.2

js combsbrothersllc.com collected 2026-10-08 18:27:26 UTC 12,585 bytes, 419 lines download raw bytes

1/**
2 * Phone track - on phones the item sliders become one scrolling row.
3 *
4 * The sliders group their items into slides on the server: a slide is a wrapper holding as many
5 * items as the element has columns. That grouping is fixed in the html, so on a narrow screen css
6 * can only wrap those items onto more rows - which is why five products end up as 2 + 2 + 1 rows
7 * inside a single slide, and a swipe jumps all five at once.
8 *
9 * Below the phone breakpoint we take the element over instead: the wrappers are removed from the
10 * layout with display:contents (css), so every item becomes a sibling in one row that the browser
11 * scrolls and snaps natively, and the arrows scroll it by one screenful.
12 *
13 * A slider that is already running when the viewport narrows - a window being resized, a phone
14 * being turned - is frozen rather than taken apart: it has no working way to be stopped, and it
15 * does not need one, because a frozen slider writes nothing. Widening again thaws it and hands
16 * the element back. An element claimed before any slider started keeps its track at every width,
17 * laid out from its own column count, since starting a slider there would mean waiting out the
18 * image preloader with the element already emptied.
19 *
20 * @since 8.2
21 */
22( function( $ )
23{
24	'use strict';
25
26	var QUERY = '(max-width: 767px)',
27		SLIDERS = '.avia-content-slider-active',
28		CLAIMED = 'av-phone-track';
29
30	function is_phone()
31	{
32		return 'function' === typeof window.matchMedia && window.matchMedia( QUERY ).matches;
33	}
34
35	function reduced_motion()
36	{
37		return 'function' === typeof window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches;
38	}
39
40	function is_rtl( track )
41	{
42		return 'rtl' === window.getComputedStyle( track ).direction;
43	}
44
45	function track_items( track )
46	{
47		return track.querySelectorAll( '.slide-entry, .products .product' );
48	}
49
50	/**
51	 * Is the item completely inside the visible part of the track?
52	 *
53	 * Asking the items is the only reliable measure here: the scrollable width holds trailing space
54	 * an item never reaches, so comparing against it keeps the next arrow lit at the end of the row,
55	 * and it says nothing about which direction the page is written in.
56	 */
57	function fully_visible( track, item )
58	{
59		if( ! item )
60		{
61			return true;
62		}
63
64		var visible = track.getBoundingClientRect(),
65			rect = item.getBoundingClientRect();
66
67		return rect.left >= visible.left - 1 && rect.right <= visible.right + 1;
68	}
69
70	/**
71	 * The arrows are hidden by css until these classes say otherwise - the slider adds them when it
72	 * starts up, so a claimed element has to do it itself.
73	 */
74	function update_arrows( $container, track )
75	{
76		var items = track_items( track ),
77			$arrows = $container.find( '.avia-slideshow-arrows' );
78
79		$arrows.toggleClass( 'av-visible-prev', items.length > 0 && ! fully_visible( track, items[0] ) );
80		$arrows.toggleClass( 'av-visible-next', items.length > 0 && ! fully_visible( track, items[ items.length - 1 ] ) );
81	}
82
83	/**
84	 * Scrolls the track itself instead of asking for behavior:'smooth'.
85	 *
86	 * A smooth scroll requested from script is ignored outright in some browsers once the element
87	 * snaps, and is not supported at all in older ones - both of which leave the arrows dead. An
88	 * own animation behaves the same everywhere. Snapping is suspended while it runs, or it fights
89	 * the animation for every frame; the position it ends on is a snap point anyway.
90	 */
91	function animate_scroll( track, target )
92	{
93		var start = track.scrollLeft,
94			distance = target - start,
95			duration = 350,
96			began = null;
97
98		if( reduced_motion() || ! window.requestAnimationFrame )
99		{
100			track.scrollLeft = target;
101			return;
102		}
103
104		track.style.scrollSnapType = 'none';
105
106		window.requestAnimationFrame( function step( now )
107		{
108			began = began || now;
109
110			var progress = Math.min( 1, ( now - began ) / duration );
111
112			//	ease out, so it settles rather than stops
113			track.scrollLeft = start + distance * ( 1 - Math.pow( 1 - progress, 3 ) );
114
115			if( progress < 1 )
116			{
117				window.requestAnimationFrame( step );
118			}
119			else
120			{
121				track.style.scrollSnapType = '';
122			}
123		} );
124	}
125
126	/**
127	 * Where the track would have to stand for this item to sit at its leading edge.
128	 */
129	function position_of( track, item )
130	{
131		return track.scrollLeft + ( item.getBoundingClientRect().left - track.getBoundingClientRect().left );
132	}
133
134	function scroll_page( track, direction )
135	{
136		var items = track_items( track ),
137			step = track.clientWidth * direction * ( is_rtl( track ) ? -1 : 1 ),
138			target = track.scrollLeft + step,
139			positions = [];
140
141		//	stop at the first and the last item rather than at the edge of the scrollable area - the
142		//	area is wider than the items reach, and scrolling into that space only snaps back.
143		//	taking both ends from the items keeps this right when the page runs right to left, where
144		//	the scroll position counts down from zero
145		Array.prototype.forEach.call( items, function( item )
146		{
147			positions.push( position_of( track, item ) );
148		} );
149
150		if( positions.length )
151		{
152			target = Math.max( Math.min.apply( null, positions ), Math.min( Math.max.apply( null, positions ), target ) );
153		}
154
155		animate_scroll( track, target );
156	}
157
158	function bind( $container, track )
159	{
160		var frame = null;
161
162		$container.on( 'click.avphonetrack', '.prev-slide, .next-slide', function( event )
163		{
164			event.preventDefault();
165			scroll_page( track, $( this ).hasClass( 'next-slide' ) ? 1 : -1 );
166		} );
167
168		track.addEventListener( 'scroll', function()
169		{
170			if( frame )
171			{
172				return;
173			}
174
175			frame = window.requestAnimationFrame( function()
176			{
177				frame = null;
178				update_arrows( $container, track );
179			} );
180		}, { passive: true } );
181
182		$( window ).on( 'debouncedresize.avphonetrack', function()
183		{
184			update_arrows( $container, track );
185		} );
186	}
187
188	/**
189	 * Freezes a slider that is running while its element becomes a track.
190	 *
191	 * Nothing is unbound - the slider has no way to be taken apart, and taking it apart is not
192	 * needed here. Its own guard flag stops it navigating, which is what would otherwise write
193	 * inline visibility onto wrappers that no longer generate a box, and the autoplay timer is
194	 * stopped so it is not ticking against a frozen slider. Both are undone in resume_slider().
195	 */
196	function freeze_slider( instance )
197	{
198		if( ! instance )
199		{
200			return;
201		}
202
203		instance.isAnimating = true;
204
205		//	whether it was rotating at this moment, which is not the same as whether autoplay is on:
206		//	a visitor who worked the arrows has already stopped it, and it must stay stopped
207		instance.av_track_was_playing = !! instance.slideshow;
208
209		if( instance.slideshow && 'function' === typeof instance.slideshow.destroy )
210		{
211			instance.slideshow.destroy();
212		}
213	}
214
215	function resume_slider( instance, track )
216	{
217		if( ! instance )
218		{
219			return;
220		}
221
222		instance.isAnimating = false;
223
224		//	while frozen it kept measuring slides that had no box, and wrote the nothing it found
225		//	onto the container - which only stayed invisible because the track css outranked it
226		track.style.height = '';
227
228		if( 'function' === typeof instance._setSize )
229		{
230			instance._setSize( true );
231		}
232
233		if( instance.av_track_was_playing && 'function' === typeof instance._startSlideshow )
234		{
235			instance._startSlideshow();
236		}
237	}
238
239	/**
240	 * @param {jQuery} $container		one slider element
241	 */
242	function claim( $container )
243	{
244		var track = $container.find( '.avia-content-slider-inner' ).first()[0];
245
246		if( ! track || $container.hasClass( CLAIMED ) )
247		{
248			return;
249		}
250
251		//	how many items the element shows per screen above the phone breakpoint - the slides the
252		//	server built are the only place that number exists in the markup, and they are about to
253		//	stop being slides. the element stays claimed after a rotation to landscape, so without
254		//	this it would have no idea how wide to make its items there.
255		//	it goes into its own property: an inline value would otherwise outrank the breakpoints
256		var per_slide = $container.find( '.slide-entry-wrap' ).first().children().length;
257
258		if( per_slide > 0 )
259		{
260			track.style.setProperty( '--av-track-cols-default', per_slide );
261		}
262
263		freeze_slider( $container.data( 'aviaSlider' ) );
264
265		//	dropping the active class is what keeps the slider out: it is both the init selector and
266		//	the hook the css uses to stack the wrappers on top of each other
267		$container.removeClass( 'avia-content-slider-active' ).addClass( CLAIMED );
268
269		//	a scrollable region needs to be reachable for people who scroll with the keyboard
270		track.setAttribute( 'tabindex', '0' );
271
272		//	display:contents drops list semantics in some browsers - the product slides are real lists
273		$( track ).find( 'ul.products' ).attr( 'role', 'list' );
274
275		//	an element that fitted its items on one slide was rendered without arrows - it keeps its
276		//	scrollbar, since nothing else would tell a visitor with a mouse that the row goes on
277		$container.toggleClass( 'av-track-no-arrows', 0 === $container.find( '.avia-slideshow-arrows a' ).length );
278
279		bind( $container, track );
280		update_arrows( $container, track );
281	}
282
283	/**
284	 * Hands the element back to the slider it was taken from.
285	 *
286	 * Only an element that had a slider is handed back. One that was claimed before any slider
287	 * started keeps its track at every width - it is laid out from the element's own column count
288	 * up there, which looks like the slider minus the animation, and starting a slider on it would
289	 * mean waiting out the image preloader with the element already emptied.
290	 */
291	function release( $container )
292	{
293		var track = $container.find( '.avia-content-slider-inner' ).first()[0],
294			instance = $container.data( 'aviaSlider' );
295
296		if( ! track || ! instance || ! $container.hasClass( CLAIMED ) )
297		{
298			return;
299		}
300
301		$container.off( '.avphonetrack' );
302		track.removeAttribute( 'tabindex' );
303		track.scrollLeft = 0;
304
305		$container.removeClass( CLAIMED ).addClass( 'avia-content-slider-active' );
306
307		resume_slider( instance, track );
308	}
309
310	function claim_all( context )
311	{
312		if( ! is_phone() )
313		{
314			return;
315		}
316
317		$( SLIDERS, context || document ).each( function()
318		{
319			claim( $( this ) );
320		} );
321	}
322
323	/**
324	 * Every element this script looks after, claimed or not - the claim takes the class the
325	 * elements are found by, so they have to be remembered rather than looked up again.
326	 */
327	var managed = $();
328
329	function watch( context )
330	{
331		managed = managed.add( $( SLIDERS, context || document ) );
332	}
333
334	var on_phone = is_phone();
335
336	function follow_viewport()
337	{
338		var phone = is_phone();
339
340		if( phone === on_phone )
341		{
342			return;
343		}
344
345		on_phone = phone;
346
347		managed.each( function()
348		{
349			var $container = $( this );
350
351			if( phone )
352			{
353				claim( $container );
354			}
355			else
356			{
357				release( $container );
358			}
359		} );
360	}
361
362	//	the scripts sit in the footer, so the markup is here and the elements can be claimed before
363	//	any ready handler - including the one that starts the sliders - gets to run
364	watch( document );
365	claim_all( document );
366
367	//	both, because neither is reliable on its own: a media query change is the precise signal but
368	//	goes undelivered in some browsers when the viewport is emulated rather than really resized,
369	//	and a resize is delivered everywhere but also fires while scrolling on a phone. following it
370	//	costs nothing when the answer has not changed
371	if( 'function' === typeof window.matchMedia )
372	{
373		var phone_query = window.matchMedia( QUERY );
374
375		if( 'function' === typeof phone_query.addEventListener )
376		{
377			phone_query.addEventListener( 'change', follow_viewport );
378		}
379		else if( 'function' === typeof phone_query.addListener )
380		{
381			//	older browsers, safari below 14 among them
382			phone_query.addListener( follow_viewport );
383		}
384	}
385
386	$( window ).on( 'resize.avphonetrack orientationchange.avphonetrack', follow_viewport );
387
388	//	should the order ever change, or a slider be started for content loaded later
389	if( $.fn.aviaSlider )
390	{
391		var start_slider = $.fn.aviaSlider;
392
393		$.fn.aviaSlider = function()
394		{
395			var $unclaimed = this.not( '.' + CLAIMED );
396
397			return $unclaimed.length ? start_slider.apply( $unclaimed, arguments ) : this;
398		};
399	}
400
401	$( function()
402	{
403		if( ! $.avia_utilities || 'function' !== typeof $.avia_utilities.activate_shortcode_scripts )
404		{
405			return;
406		}
407
408		var activate = $.avia_utilities.activate_shortcode_scripts;
409
410		$.avia_utilities.activate_shortcode_scripts = function( container )
411		{
412			watch( container );
413			claim_all( container );
414
415			return activate.apply( this, arguments );
416		};
417	} );
418
419} )( jQuery );

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.