PageSourceSearch

https://az-titan.ru/wp-content/themes/titan/assets/js/modules/services.js

js az-titan.ru collected 2026-10-07 17:31:23 UTC 7,768 bytes, 254 lines download raw bytes

1const $ = window.jQuery;
2
3const SERVICES_MOBILE_BREAKPOINT = 750;
4
5function initServices() {
6	const cards = document.querySelectorAll(".js_service_list__card");
7
8	if (cards.length === 0) return;
9
10	if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") return;
11	gsap.registerPlugin(ScrollTrigger);
12
13	const windowWidth = window.innerWidth;
14	const isMobile = windowWidth <= SERVICES_MOBILE_BREAKPOINT;
15
16	const cardConfigs = [];
17	const scrollTriggers = [];
18
19	// ШАГ 1: Получаем все координаты и высоты ДО сворачивания карточек
20	cards.forEach((card, index) => {
21		const inner = card.querySelector(".service_list__card__inner");
22		if (!inner) return;
23
24		const cardRect = card.getBoundingClientRect();
25		const scrollY = window.scrollY || window.pageYOffset || document.documentElement.scrollTop;
26		const cardTop = cardRect.top + scrollY;
27		const cardBottom = cardRect.bottom + scrollY;
28
29		const getCardHeight = () => {
30			const originalMaxHeight = card.style.maxHeight;
31			const originalOverflow = card.style.overflow;
32			card.style.maxHeight = "none";
33			card.style.overflow = "visible";
34			const height = inner.offsetHeight + parseFloat(getComputedStyle(card).paddingTop || 0) + parseFloat(getComputedStyle(card).paddingBottom || 0);
35			card.style.maxHeight = originalMaxHeight;
36			card.style.overflow = originalOverflow;
37			return height;
38		};
39
40		const targetHeight = getCardHeight();
41
42		cardConfigs.push({
43			card,
44			inner,
45			index,
46			cardTop,
47			cardBottom,
48			targetHeight,
49			getCardHeight
50		});
51	});
52
53	// ШАГ 2: Устанавливаем сворачивание для всех карточек
54	const windowHeight = window.innerHeight;
55	const collapsedHeight = windowWidth <= 1024 ? "2.5rem" : "3rem";
56	cardConfigs.forEach((config) => {
57		gsap.set(config.card, {
58			maxHeight: collapsedHeight,
59			opacity: 1
60		});
61	});
62
63	// Мобильная версия: разворот по одному разу при появлении в зоне видимости (без scrub — не тормозит)
64	if (isMobile) {
65		cardConfigs.forEach((config) => {
66			const { card, targetHeight } = config;
67			const initialHeight = "2.5rem";
68			gsap.fromTo(
69				card,
70				{ maxHeight: initialHeight },
71				{
72					maxHeight: targetHeight + "px",
73					ease: "power2.out",
74					duration: 0.5,
75					scrollTrigger: {
76						trigger: card,
77						start: "top 85%",
78						toggleActions: "play none none none",
79						once: true
80					}
81				}
82			);
83		});
84		ScrollTrigger.refresh();
85		return;
86	}
87
88	// ШАГ 3: Десктоп — ScrollTrigger с scrub для каждой карточки
89	cardConfigs.forEach((config, index) => {
90		const { card, cardTop, targetHeight } = config;
91
92		const startOffset = windowHeight * 0.6; // 60% от верха экрана для start
93		const endOffset = windowHeight * 0.6; // 60% от верха экрана для end
94
95		let startPosition, endPosition;
96
97		if (index === 0) {
98			startPosition = cardTop - startOffset;
99			const expandedCardBottom = cardTop + targetHeight;
100			endPosition = expandedCardBottom - endOffset;
101		} else {
102			const prevST = scrollTriggers[index - 1];
103			if (prevST) {
104				startPosition = prevST.end;
105				const expandedCardBottom = cardTop + targetHeight;
106				endPosition = expandedCardBottom - endOffset;
107			} else {
108				startPosition = cardTop - startOffset;
109				const expandedCardBottom = cardTop + targetHeight;
110				endPosition = expandedCardBottom - endOffset;
111			}
112		}
113
114		const initialHeight = windowWidth <= 1024 ? "2.5rem" : "3rem";
115		
116		// Создаем анимацию для разворачивания карточки
117		const expandAnimation = gsap.fromTo(card, 
118			{ maxHeight: initialHeight },
119			{ 
120				maxHeight: targetHeight + "px",
121				ease: "power2.out",
122				paused: true
123			}
124		);
125
126		// Создаем ScrollTrigger
127		const st = ScrollTrigger.create({
128			trigger: card,
129			start: startPosition,
130			end: endPosition,
131			scrub: 1.5,
132			animation: expandAnimation
133		});
134
135		scrollTriggers[index] = st;
136
137		// Обновляем ScrollTrigger после создания каждого триггера
138		ScrollTrigger.refresh();
139
140		// Для последующих карточек пересчитываем end с учетом текущей позиции после разворачивания предыдущих
141		if (index > 0 && scrollTriggers[index - 1]) {
142			// Получаем текущую позицию карточки (после разворачивания предыдущих)
143			const cardRect = card.getBoundingClientRect();
144			const scrollY = window.scrollY || window.pageYOffset || document.documentElement.scrollTop;
145			const currentCardTop = cardRect.top + scrollY;
146			const windowHeight = window.innerHeight;
147			const endOffset = windowHeight * 0.6; // 60% от верха экрана для end
148			
149			// Пересчитываем end: когда низ развернутой карточки достигнет 60% от верха экрана
150			const expandedCardBottom = currentCardTop + targetHeight;
151			const newEnd = expandedCardBottom - endOffset;
152			
153			// Обновляем end позицию
154			st.setPositions(st.start, newEnd);
155			
156			// Обновляем ScrollTrigger после изменения позиций
157			ScrollTrigger.refresh();
158		}
159
160		// Обновляем высоту при изменении размера окна
161		const updateHeight = () => {
162			const newTargetHeight = config.getCardHeight();
163			const currentWindowHeight = window.innerHeight;
164			const newInitialHeight = currentWindowHeight <= 1024 ? "2.5rem" : "3rem";
165			
166			// Обновляем начальную высоту в анимации
167			expandAnimation.vars.maxHeight = newTargetHeight + "px";
168			expandAnimation.fromVars.maxHeight = newInitialHeight;
169			
170			ScrollTrigger.refresh();
171		};
172
173		window.addEventListener("resize", updateHeight);
174	});
175}
176
177
178function initHorizontalDragScroll(selector, draggingClass) {
179	const el = document.querySelector(selector);
180	if (!el) return;
181
182	let isDown = false;
183	let startX;
184	let scrollLeft;
185
186	el.addEventListener("mousedown", (e) => {
187		isDown = true;
188		el.classList.add(draggingClass);
189		startX = e.pageX;
190		scrollLeft = el.scrollLeft;
191	});
192
193	el.addEventListener("mouseleave", () => {
194		isDown = false;
195		el.classList.remove(draggingClass);
196	});
197
198	el.addEventListener("mouseup", () => {
199		isDown = false;
200		el.classList.remove(draggingClass);
201	});
202
203	el.addEventListener("mousemove", (e) => {
204		if (!isDown) return;
205		e.preventDefault();
206		const walk = (e.pageX - startX) * 1.2;
207		el.scrollLeft = scrollLeft - walk;
208	});
209}
210
211function initWorkWithBigDragScroll() {
212	initHorizontalDragScroll(
213		".work_with_big__images",
214		"work_with_big__images--dragging",
215	);
216}
217
218function initIsolationImagesDragScroll() {
219	initHorizontalDragScroll(
220		".isolation_images__line",
221		"isolation_images__line--dragging",
222	);
223}
224
225export default function services() {
226	return {
227		init: function () {
228			$(document).ready(function () {
229				initWorkWithBigDragScroll();
230				initIsolationImagesDragScroll();
231			});
232
233			// Ждем загрузки DOM и GSAP
234			if (typeof gsap !== "undefined" && typeof ScrollTrigger !== "undefined") {
235				// Небольшая задержка для гарантии загрузки всех стилей
236				setTimeout(() => {
237					initServices();
238				}, 100);
239			} else {
240				// Если GSAP еще не загружен, ждем
241				$(document).ready(function () {
242					const checkGSAP = setInterval(() => {
243						if (typeof gsap !== "undefined" && typeof ScrollTrigger !== "undefined") {
244							clearInterval(checkGSAP);
245							setTimeout(() => {
246								initServices();
247							}, 100);
248						}
249					}, 50);
250				});
251			}
252		},
253	};
254}

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.