PageSourceSearch

https://www.unirioja.es/wp-content/plugins/ele-custom-skin/modules…/assets/js/ecs-slider.js?ver=4.3.8

js unirioja.es collected 2026-09-24 07:36:32 UTC 18,235 bytes, 490 lines download raw bytes

1( function () {
2	'use strict';
3
4	var CHEVRON_PREV = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 25"><path d="M15.5 5 8.5 12.5 15.5 20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>';
5	var CHEVRON_NEXT = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 25"><path d="M9.5 5 16.5 12.5 9.5 20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>';
6
7	// ── Live mode (PHP generated Swiper HTML) ─────────────────────────────────
8
9	function doInitLiveSlider( el ) {
10		el.setAttribute( 'data-ecs-swiper-init', '1' );
11
12		var settings = JSON.parse( el.getAttribute( 'data-ecs-slider-settings' ) || '{}' );
13
14		if ( settings.navigation ) {
15			settings.navigation = {
16				prevEl: el.querySelector( '.elementor-swiper-button-prev' ),
17				nextEl: el.querySelector( '.elementor-swiper-button-next' ),
18			};
19		}
20		if ( settings.pagination ) {
21			settings.pagination.el = el.querySelector( '.swiper-pagination' );
22		}
23
24		new Swiper( el, settings ); // eslint-disable-line no-undef
25	}
26
27	function initLiveSlider( el ) {
28		if ( el.hasAttribute( 'data-ecs-swiper-init' ) ) {
29			return;
30		}
31
32		// Defer init if hidden (e.g. inside display:none responsive-version on desktop).
33		// ResizeObserver fires when CSS shows the element — media query triggered by viewport change.
34		if ( el.offsetWidth === 0 && typeof ResizeObserver !== 'undefined' ) {
35			el.setAttribute( 'data-ecs-swiper-init', 'pending' );
36			var ro = new ResizeObserver( function ( entries ) {
37				if ( entries[ 0 ].contentRect.width > 0 ) {
38					ro.disconnect();
39					el.removeAttribute( 'data-ecs-swiper-init' );
40					doInitLiveSlider( el );
41				}
42			} );
43			ro.observe( el );
44			return;
45		}
46
47		doInitLiveSlider( el );
48	}
49
50	function initAllLiveSliders() {
51		document.querySelectorAll( '.ecs-swiper:not([data-ecs-swiper-init])' ).forEach( initLiveSlider );
52	}
53
54	// ── Editor mode (JS builds Swiper from existing DOM) ──────────────────────
55
56	/**
57	 * Read slider control values from the parent editor's Backbone model.
58	 * This is the same pattern used in ecs-editor-preview.js.
59	 */
60	function getEditorSettings( containerId ) {
61		try {
62			return window.parent.elementor.getContainer( containerId ).settings.toJSON();
63		} catch ( e ) {
64			return {};
65		}
66	}
67
68	function buildSwiperConfig( s ) {
69		var navigation = s.ecs_navigation || 'arrows';
70		var showArrows = navigation === 'arrows' || navigation === 'both';
71		var showDots   = navigation === 'dots'   || navigation === 'both';
72
73		var colsDesktop = parseInt( s.ecs_slider_columns, 10 ) || 1;
74		var colsTablet  = parseInt( s.ecs_slider_columns_tablet, 10 ) || colsDesktop;
75		var colsMobile  = parseInt( s.ecs_slider_columns_mobile, 10 ) || colsTablet;
76		var layoutTablet = s.ecs_container_type_tablet || '';
77		var layoutMobile = s.ecs_container_type_mobile || '';
78
79		var config = {
80			slidesPerView: colsDesktop,
81			loop:          s.ecs_loop === 'yes',
82			speed:         parseInt( s.ecs_speed, 10 ) || 500,
83			spaceBetween:  parseInt( s.ecs_space_between, 10 ) || 0,
84			breakpoints: {
85				0: {
86					slidesPerView: colsMobile,
87					enabled: layoutMobile !== 'flex' && layoutMobile !== 'grid',
88				},
89				768: {
90					slidesPerView: colsTablet,
91					enabled: layoutTablet !== 'flex' && layoutTablet !== 'grid',
92				},
93				1025: {
94					slidesPerView: colsDesktop,
95					enabled: true,
96				},
97			},
98		};
99
100		if ( s.ecs_autoplay === 'yes' ) {
101			config.autoplay = {
102				delay:                parseInt( s.ecs_autoplay_speed, 10 ) || 3000,
103				pauseOnMouseEnter:    s.ecs_pause_on_hover === 'yes',
104				disableOnInteraction: false,
105			};
106		}
107
108		return { config: config, showArrows: showArrows, showDots: showDots };
109	}
110
111	/**
112	 * Move real children back to their scope (e-con-inner or container) then remove swiper.
113	 * Using real elements (not clones) keeps Backbone model-binding intact and allows editing.
114	 */
115	function destroyEditorSlider( containerEl ) {
116		var existing = containerEl.querySelector( ':scope > .ecs-editor-swiper' );
117		if ( existing ) {
118			var inner = containerEl.querySelector( ':scope > .e-con-inner' );
119			var scope = inner || containerEl;
120			// Move real children back from slides to scope, preserving order
121			existing.querySelectorAll( ':scope > .swiper-wrapper > .swiper-slide:not(.swiper-slide-duplicate)' ).forEach( function ( slide ) {
122				var child = Array.from( slide.children ).find( function ( el ) {
123					return el.dataset && el.dataset.id;
124				} );
125				if ( child ) {
126					scope.appendChild( child );
127				}
128			} );
129			if ( existing.swiper ) {
130				existing.swiper.destroy( true, true );
131			}
132			existing.remove();
133		}
134		containerEl.classList.remove( 'ecs-editor-slider-active' );
135		containerEl.removeAttribute( 'data-ecs-slider-key' );
136		containerEl.removeAttribute( 'data-ecs-rebuild-key' );
137	}
138
139	/** Cache key based on child IDs only (structural changes). */
140	function sliderKey( children ) {
141		return children.map( function ( el ) { return el.getAttribute( 'data-id' ); } ).join( ',' );
142	}
143
144	/**
145	 * Key for settings that require HTML rebuild (navigation type).
146	 * Non-structural settings (columns, speed…) are updated in-place by
147	 * updateEditorSliderParams() to avoid disturbing the live DOM.
148	 */
149	function rebuildKey( built ) {
150		return ( built.showArrows ? 'a' : '' ) + ( built.showDots ? 'd' : '' );
151	}
152
153	/**
154	 * Build editor swiper by MOVING real children into slides (not cloning).
155	 * Real elements stay connected to their Backbone views so they remain editable.
156	 * Loop is disabled in editor to prevent Swiper from creating duplicate [data-id] nodes.
157	 */
158	function buildEditorSlider( containerEl ) {
159		var id       = containerEl.getAttribute( 'data-id' );
160		var settings = getEditorSettings( id );
161
162		var inner    = containerEl.querySelector( ':scope > .e-con-inner' );
163		var scope    = inner || containerEl;
164		var children = Array.from( scope.children ).filter( function ( el ) {
165			return el.dataset && el.dataset.id;
166		} );
167
168		if ( ! children.length ) {
169			return;
170		}
171
172		var built      = buildSwiperConfig( settings );
173		built.config.loop = false; // disable loop in editor — avoids duplicate [data-id] nodes
174
175		var swiperEl   = document.createElement( 'div' );
176		swiperEl.className = 'swiper ecs-swiper ecs-editor-swiper';
177
178		var wrapperEl  = document.createElement( 'div' );
179		wrapperEl.className = 'swiper-wrapper';
180
181		// Move real children into slides — preserves Backbone binding and allows editing
182		children.forEach( function ( child ) {
183			var slide = document.createElement( 'div' );
184			slide.className = 'swiper-slide';
185			slide.appendChild( child ); // MOVE, not clone
186			wrapperEl.appendChild( slide );
187		} );
188		swiperEl.appendChild( wrapperEl );
189
190		if ( built.showArrows ) {
191			swiperEl.insertAdjacentHTML( 'beforeend',
192				'<div class="elementor-swiper-button elementor-swiper-button-prev" role="button" tabindex="0">' + CHEVRON_PREV + '</div>' +
193				'<div class="elementor-swiper-button elementor-swiper-button-next" role="button" tabindex="0">' + CHEVRON_NEXT + '</div>'
194			);
195			built.config.navigation = {
196				prevEl: swiperEl.querySelector( '.elementor-swiper-button-prev' ),
197				nextEl: swiperEl.querySelector( '.elementor-swiper-button-next' ),
198			};
199		}
200
201		if ( built.showDots ) {
202			swiperEl.insertAdjacentHTML( 'beforeend', '<div class="swiper-pagination"></div>' );
203			built.config.pagination = {
204				el:        swiperEl.querySelector( '.swiper-pagination' ),
205				clickable: true,
206			};
207		}
208
209		// CSS class hides empty .e-con-inner (children were moved out into swiper slides)
210		containerEl.classList.add( 'ecs-editor-slider-active' );
211		containerEl.appendChild( swiperEl );
212		containerEl.setAttribute( 'data-ecs-slider-key', sliderKey( children ) );
213		containerEl.setAttribute( 'data-ecs-rebuild-key', rebuildKey( built ) );
214
215		if ( typeof Swiper !== 'undefined' ) { // eslint-disable-line no-undef
216			new Swiper( swiperEl, built.config ); // eslint-disable-line no-undef
217		}
218	}
219
220	function syncEditorSlider( containerEl ) {
221		// Backbone re-render may have removed .ecs-editor-swiper while leaving
222		// ecs-editor-slider-active class — reset so we fall through to rebuild.
223		if (
224			containerEl.classList.contains( 'ecs-editor-slider-active' ) &&
225			! containerEl.querySelector( ':scope > .ecs-editor-swiper' )
226		) {
227			destroyEditorSlider( containerEl );
228		}
229
230		var swiperEl = containerEl.querySelector( ':scope > .ecs-editor-swiper' );
231		var inner    = containerEl.querySelector( ':scope > .e-con-inner' );
232		var scope    = inner || containerEl;
233
234		var children;
235		if ( swiperEl ) {
236			// Slider active: real children live inside swiper slides
237			var slideChildren = Array.from(
238				swiperEl.querySelectorAll( ':scope > .swiper-wrapper > .swiper-slide:not(.swiper-slide-duplicate)' )
239			).map( function ( slide ) {
240				return Array.from( slide.children ).find( function ( el ) {
241					return el.dataset && el.dataset.id;
242				} );
243			} ).filter( Boolean );
244
245			// Plus any new direct scope children Backbone may have appended (new widget added)
246			var newScopeChildren = Array.from( scope.children ).filter( function ( el ) {
247				return el.dataset && el.dataset.id;
248			} );
249			children = slideChildren.concat( newScopeChildren );
250		} else {
251			children = Array.from( scope.children ).filter( function ( el ) {
252				return el.dataset && el.dataset.id;
253			} );
254		}
255
256		var cached = containerEl.getAttribute( 'data-ecs-slider-key' );
257		if ( cached !== null && cached === sliderKey( children ) ) {
258			return; // structure unchanged
259		}
260
261		destroyEditorSlider( containerEl ); // moves children back to scope
262		if ( children.length ) {
263			buildEditorSlider( containerEl );
264		}
265	}
266
267	/**
268	 * Return the resolved ecs_container_type for the current device mode.
269	 * Inherits from larger breakpoints when device-specific value is empty.
270	 * Mirrors the same logic in ecs-editor-preview.js (parent frame).
271	 */
272	function getResolvedType( containerEl ) {
273		var id = containerEl.getAttribute( 'data-id' );
274		if ( ! id ) { return 'flex'; }
275		try {
276			var device   = window.parent.elementor.channels.deviceMode.request( 'currentMode' ) || 'desktop';
277			var settings = window.parent.elementor.getContainer( id ).settings;
278			var suffixes = { desktop: '', tablet: '_tablet', mobile: '_mobile' };
279			var order    = device === 'mobile'  ? [ 'mobile', 'tablet', 'desktop' ]
280			             : device === 'tablet'  ? [ 'tablet', 'desktop' ]
281			             : [ 'desktop' ];
282			for ( var i = 0; i < order.length; i++ ) {
283				var val = settings.get( 'ecs_container_type' + suffixes[ order[ i ] ] );
284				if ( val ) { return val; }
285			}
286			return 'flex';
287		} catch ( e ) { return 'flex'; }
288	}
289
290	function syncAllEditorSliders() {
291		// Cleanup or rebuild stale active markers.
292		document.querySelectorAll( '.ecs-editor-slider-active' ).forEach( function ( el ) {
293			if ( getResolvedType( el ) !== 'slider' ) {
294				destroyEditorSlider( el );
295			} else if ( ! el.querySelector( ':scope > .ecs-editor-swiper' ) ) {
296				// Elementor re-rendered and removed our swiper DOM — rebuild it.
297				destroyEditorSlider( el ); // clears stale ecs-editor-slider-active class
298				syncEditorSlider( el );
299			}
300		} );
301		// Build: any container with an ECS slider class that resolves to 'slider' now.
302		document.querySelectorAll( '.e-con[class*="-slider"]' ).forEach( function ( el ) {
303			if ( ! el.classList.contains( 'ecs-editor-slider-active' ) && getResolvedType( el ) === 'slider' ) {
304				syncEditorSlider( el );
305			}
306		} );
307	}
308
309	/**
310	 * Update Swiper config in-place without destroy/clone.
311	 * Called on settings panel changes (columns, speed, color, etc.).
312	 * Falls back to full syncEditorSlider only when HTML structure must change
313	 * (navigation type or loop — these require different DOM elements).
314	 */
315	function updateEditorSliderParams( containerEl ) {
316		var swiperEl = containerEl.querySelector( ':scope > .ecs-editor-swiper' );
317		if ( ! swiperEl || ! swiperEl.swiper ) {
318			syncEditorSlider( containerEl ); // swiper missing — full rebuild
319			return;
320		}
321
322		var id       = containerEl.getAttribute( 'data-id' );
323		var settings = getEditorSettings( id );
324		var built    = buildSwiperConfig( settings );
325		var swiper   = swiperEl.swiper;
326
327		// Navigation type or loop change requires HTML rebuild (different DOM structure)
328		var currRebuildKey = rebuildKey( built );
329		var prevRebuildKey = containerEl.getAttribute( 'data-ecs-rebuild-key' );
330		if ( prevRebuildKey !== null && prevRebuildKey !== currRebuildKey ) {
331			syncEditorSlider( containerEl );
332			return;
333		}
334
335		// Safe in-place param update — no cloning, no race condition
336		swiper.params.slidesPerView = built.config.slidesPerView;
337		swiper.params.speed         = built.config.speed;
338		swiper.params.spaceBetween  = built.config.spaceBetween;
339		swiper.params.breakpoints   = built.config.breakpoints;
340
341		if ( built.config.autoplay ) {
342			swiper.params.autoplay = built.config.autoplay;
343			if ( swiper.autoplay && ! swiper.autoplay.running ) {
344				swiper.autoplay.start();
345			}
346		} else if ( swiper.autoplay && swiper.autoplay.running ) {
347			swiper.autoplay.stop();
348			swiper.params.autoplay = false;
349		}
350
351		swiper.update();
352	}
353
354	function updateAllEditorSliderParams() {
355		// Clear the structural key so syncEditorSlider always rebuilds with
356		// fresh settings (column count, speed, etc. may have changed).
357		document.querySelectorAll( '.ecs-editor-slider-active' ).forEach( function ( el ) {
358			el.removeAttribute( 'data-ecs-slider-key' );
359			syncEditorSlider( el );
360		} );
361	}
362
363	// ── Entry point ───────────────────────────────────────────────────────────
364
365	var inEditMode = !! (
366		window.elementorFrontend &&
367		typeof elementorFrontend.isEditMode === 'function' &&
368		elementorFrontend.isEditMode()
369	);
370
371	if ( inEditMode ) {
372		var suppressObserver = false;
373
374		var observer = new MutationObserver( function ( mutations ) {
375			if ( suppressObserver ) { return; }
376
377			var relevant = false;
378			for ( var i = 0; i < mutations.length; i++ ) {
379				var m = mutations[ i ];
380				var t = m.target;
381				// Skip mutations caused by our own swiper structure
382				if ( t && t.classList && (
383					t.classList.contains( 'ecs-editor-swiper' ) ||
384					t.classList.contains( 'swiper-wrapper' ) ||
385					t.classList.contains( 'swiper-slide' )
386				) ) {
387					continue;
388				}
389				relevant = true;
390				break;
391			}
392			if ( relevant ) {
393				suppressObserver = true;
394				syncAllEditorSliders();
395				Promise.resolve().then( function () { suppressObserver = false; } );
396			}
397		} );
398
399		function setupEditor() {
400			syncAllEditorSliders();
401			observer.observe( document.body, {
402				childList:       true,
403				subtree:         true,
404				attributes:      true,
405				attributeFilter: [ 'class', 'data-id' ],
406			} );
407
408			// Safety rebuild at 800ms: catches cases where MutationObserver fires before
409			// Backbone has finished populating widget content. syncEditorSlider will
410			// also detect empty widgets on each MO-triggered call going forward.
411			setTimeout( syncAllEditorSliders, 800 );
412
413			// Settings panel changes (columns, colors, speed…) don't mutate the DOM so
414			// MutationObserver never fires. Update Swiper params in-place — no cloning,
415			// no race condition with Backbone re-renders.
416			try {
417				var settingsTimer = null;
418				// editor:change fires for panel control edits in real usage.
419				window.parent.elementor.channels.editor.on( 'change', function () {
420					clearTimeout( settingsTimer );
421					settingsTimer = setTimeout( updateAllEditorSliderParams, 150 );
422				} );
423				// command:after fires for every $e.run() including programmatic changes.
424				window.parent.elementor.channels.data.on( 'command:after', function () {
425					clearTimeout( settingsTimer );
426					settingsTimer = setTimeout( updateAllEditorSliderParams, 150 );
427				} );
428				// Device mode switch changes which type is active — rebuild sliders.
429				window.parent.elementor.channels.deviceMode.on( 'change', function () {
430					clearTimeout( settingsTimer );
431					settingsTimer = setTimeout( syncAllEditorSliders, 200 );
432				} );
433			} catch ( e ) { /* cross-origin or elementor unavailable */ }
434		}
435
436		// Expose a hook for ecs-editor-preview.js (parent frame) to call when
437		// settings change. Cross-frame Backbone channels are unreliable for this.
438		window.ecsSliderSettingsChanged = function () {
439			// Re-sync existing active sliders with updated params.
440			document.querySelectorAll( '.ecs-editor-slider-active' ).forEach( function ( el ) {
441				el.removeAttribute( 'data-ecs-slider-key' );
442				syncEditorSlider( el );
443			} );
444			// Build any new sliders that just became active (e.g. type just set to 'slider').
445			syncAllEditorSliders();
446		};
447
448		// components:init already fired by the time a footer script loads.
449		// Run directly — Backbone views are ready by DOMContentLoaded.
450		if ( document.readyState === 'loading' ) {
451			document.addEventListener( 'DOMContentLoaded', function () {
452				setTimeout( setupEditor, 300 );
453			} );
454		} else {
455			setTimeout( setupEditor, 300 );
456		}
457
458	} else {
459		// Live frontend: PHP already generated Swiper HTML.
460		if ( document.readyState === 'loading' ) {
461			document.addEventListener( 'DOMContentLoaded', initAllLiveSliders );
462		} else {
463			initAllLiveSliders();
464		}
465
466		if ( typeof MutationObserver !== 'undefined' ) {
467			var liveObserver = new MutationObserver( function ( mutations ) {
468				var found = false;
469				for ( var i = 0; i < mutations.length; i++ ) {
470					var added = mutations[ i ].addedNodes;
471					for ( var j = 0; j < added.length; j++ ) {
472						var node = added[ j ];
473						if ( node.nodeType !== 1 ) { continue; }
474						if (
475							( node.classList && node.classList.contains( 'ecs-swiper' ) ) ||
476							( node.querySelector && node.querySelector( '.ecs-swiper:not([data-ecs-swiper-init])' ) )
477						) {
478							found = true;
479							break;
480						}
481					}
482					if ( found ) { break; }
483				}
484				if ( found ) { initAllLiveSliders(); }
485			} );
486			liveObserver.observe( document.body, { childList: true, subtree: true } );
487		}
488	}
489
490} )();

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.