PageSourceSearch

https://www.projectwizards.net/assets/js/merlin-project.1781687432.js

js projectwizards.net collected 2026-10-02 03:33:34 UTC 13,705 bytes, 336 lines download raw bytes

1document.addEventListener('DOMContentLoaded', function() {
2	// Problem/Solution interactive panel — buttons swap screenshots
3	(function() {
4		var panels = document.querySelectorAll('.mp-problem__panel');
5		panels.forEach(function(panel) {
6			var buttons = panel.querySelectorAll('.mp-problem__btn');
7			var shots = panel.querySelectorAll('.mp-problem__shot');
8			var section = panel.closest('.mp-problem');
9			var intro = section ? section.querySelector('.mp-problem__intro') : null;
10			var titleEl = section ? section.querySelector('.mp-problem__title') : null;
11			var titleSpans = titleEl ? titleEl.querySelectorAll('span') : [];
12			function activate(target) {
13				if(target === panel.dataset.active) return;
14				panel.dataset.active = target;
15				buttons.forEach(function(b) {
16					var on = b.dataset.target === target;
17					b.classList.toggle('is-active', on);
18					b.setAttribute('aria-selected', on ? 'true' : 'false');
19					if(on) {
20						if(intro && b.dataset.detail) {
21							intro.style.opacity = '0';
22							setTimeout(function() {
23								intro.textContent = b.dataset.detail;
24								intro.style.opacity = '1';
25							}, 100);
26						}
27						if(titleSpans.length >= 2 && b.dataset.title1) {
28							titleEl.style.opacity = '0';
29							setTimeout(function() {
30								titleSpans[0].textContent = b.dataset.title1;
31								titleSpans[1].textContent = b.dataset.title2;
32								titleEl.style.opacity = '1';
33							}, 100);
34						}
35					}
36				});
37				shots.forEach(function(s) {
38					s.classList.toggle('is-active', s.dataset.img === target);
39				});
40			}
41			// Set initial text to match the active button
42			var activeBtn = panel.querySelector('.mp-problem__btn.is-active');
43			if(activeBtn && intro && activeBtn.dataset.detail) {
44				intro.textContent = activeBtn.dataset.detail;
45			}
46			buttons.forEach(function(b) {
47				b.addEventListener('click', function() { activate(b.dataset.target); });
48			});
49		});
50	})();
51
52	// Animated morphing of the Gantt group bar (path d attribute updated via JS)
53	function animateGtGroup(illu) {
54		var path = illu.querySelector('.gt-group');
55		var progPath = illu.querySelector('.gt-group-prog');
56		if(!path) return;
57		var startDelay = 0, dur = 1200;
58		var p0 = 200, p2 = 336;
59		function makeD(e) {
60			return 'M 88 72 L ' + (e - 8) + ' 72 Q ' + e + ' 72 ' + e + ' 80 L ' + e + ' 86 C ' + (e - 8) + ' 100 ' + (e - 8) + ' 100 ' + (e - 16) + ' 86 L 96 86 C 88 100 88 100 80 86 L 80 80 Q 80 72 88 72 Z';
61		}
62		function makeProgD(e) {
63			var progEnd = 80 + 0.75 * (e - 80);
64			return 'M 88 72 L ' + progEnd + ' 72 L ' + progEnd + ' 86 L 96 86 C 88 100 88 100 80 86 L 80 80 Q 80 72 88 72 Z';
65		}
66		function update(e) {
67			path.setAttribute('d', makeD(e));
68			if(progPath) progPath.setAttribute('d', makeProgD(e));
69		}
70		function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
71		var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
72		if(reduced) { update(p2); return; }
73		var playId = (illu._gtPlayId = (illu._gtPlayId || 0) + 1);
74		update(p0);
75		var start = null;
76		function step(now) {
77			if(playId !== illu._gtPlayId) return;
78			if(start === null) start = now;
79			var t = now - start;
80			var e;
81			if(t < startDelay) e = p0;
82			else if(t < startDelay + dur) e = p0 + (p2 - p0) * easeOut((t - startDelay) / dur);
83			else e = p2;
84			update(e);
85			if(t < startDelay + dur) requestAnimationFrame(step);
86		}
87		requestAnimationFrame(step);
88	}
89	// Drag der Kanban-Karte in die Done-Spalte. Bewegung ueber das SVG-transform-
90	// Attribut (User-Units) statt CSS-px-Transform, damit Start- und Endposition bei
91	// jedem Browser-Zoom exakt mit der viewBox skalieren (CSS-px-Transforms ueber-
92	// schiessen in WebKit unter Zoom). Geometrie der Karte liegt im IP-Slot 2; das
93	// Ziel +230/-102 (User-Units) entspricht der Done-Spalte.
94	function animateKbDrag(illu) {
95		var card = illu.querySelector('.kb-card-3');
96		if(!card) return;
97		var startDelay = 1600, dur = 900;
98		var tx = 230, ty = -102;
99		function setT(x, y) { card.setAttribute('transform', 'translate(' + x + ' ' + y + ')'); }
100		// Annaeherung an cubic-bezier(0.4, 0, 0.2, 1) (Material-Standard).
101		function easeInOut(t) { return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; }
102		var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
103		if(reduced) { setT(tx, ty); return; }
104		var playId = (illu._kbPlayId = (illu._kbPlayId || 0) + 1);
105		setT(0, 0);
106		var start = null;
107		function step(now) {
108			if(playId !== illu._kbPlayId) return;
109			if(start === null) start = now;
110			var t = now - start;
111			var x, y;
112			if(t < startDelay) { x = 0; y = 0; }
113			else if(t < startDelay + dur) {
114				var p = easeInOut((t - startDelay) / dur);
115				x = tx * p; y = ty * p;
116			} else { x = tx; y = ty; }
117			setT(x, y);
118			if(t < startDelay + dur) requestAnimationFrame(step);
119		}
120		requestAnimationFrame(step);
121	}
122	function triggerIllu(illu) {
123		if(!illu) return;
124		illu.classList.remove('play');
125		void illu.offsetWidth;
126		illu.classList.add('play');
127		if(illu.classList.contains('gt')) animateGtGroup(illu);
128		if(illu.classList.contains('kb')) animateKbDrag(illu);
129	}
130	// Kick off animation on initially active panel only once the visual itself scrolls into view
131	var initialIllu = document.querySelector('.mp-panel.active .mp-illu');
132	var featuresPlayed = false;
133	function playActivePanel() {
134		var illu = document.querySelector('.mp-panel.active .mp-illu');
135		if(!illu) return;
136		illu.classList.add('play');
137		if(illu.classList.contains('gt')) animateGtGroup(illu);
138		if(illu.classList.contains('kb')) animateKbDrag(illu);
139	}
140	if(initialIllu && 'IntersectionObserver' in window) {
141		var featuresObserver = new IntersectionObserver(function(entries) {
142			entries.forEach(function(entry) {
143				if(entry.isIntersecting && !featuresPlayed) {
144					featuresPlayed = true;
145					playActivePanel();
146					featuresObserver.disconnect();
147				}
148			});
149		}, { threshold: 0.4 });
150		featuresObserver.observe(initialIllu);
151	} else {
152		playActivePanel();
153	}
154	// Tab switching
155	document.querySelectorAll('.mp-tab').forEach(function(tab) {
156		tab.addEventListener('click', function() {
157			document.querySelectorAll('.mp-tab').forEach(function(t) { t.classList.remove('active'); });
158			document.querySelectorAll('.mp-panel').forEach(function(p) { p.classList.remove('active'); });
159			this.classList.add('active');
160			var panelId = this.getAttribute('data-panel');
161			var panel = document.getElementById(panelId);
162			if(panel) panel.classList.add('active');
163			var illu = panel && panel.querySelector('.mp-illu');
164			triggerIllu(illu);
165		});
166	});
167
168	// Review modal
169	var reviewsData = [];
170	var reviewsEl = document.getElementById('mpReviewsData');
171	if (reviewsEl) { try { reviewsData = JSON.parse(reviewsEl.textContent); } catch(e) {} }
172	var modal = document.getElementById('mpReviewModal');
173	var cfg = window.mpConfig || {};
174	var tr = cfg.translations || {};
175	var currentLang = cfg.lang || 'en';
176	var langNames = tr.translated || {};
177	var sourceLabels = tr.sourceLabels || { appstore: 'App Store', capterra: 'Capterra', reference: 'Reference' };
178	var sourceLinkLabels = tr.sourceLinks || { appstore: 'View on App Store', capterra: 'View on Capterra', reference: 'View reference' };
179	var sourceFallbackUrls = {
180		appstore:  'https://apps.apple.com/app/id1335639877',
181		capterra:  'https://www.capterra.com/p/89688/Merlin-Project/#reviews',
182		reference: '#'
183	};
184
185	document.querySelectorAll('.mp-bento__card[data-review], .rv-card[data-review]').forEach(function(card) {
186		card.addEventListener('click', function() {
187			var idx = parseInt(this.getAttribute('data-review'));
188			var r = reviewsData[idx];
189			if (!r || !modal) return;
190			document.getElementById('mpModalTitle').textContent = r.title || '';
191			document.getElementById('mpModalBody').textContent = r.body || '';
192			var starsEl = modal.querySelector('.mp-modal__stars');
193			if (starsEl) {
194				var rt = parseInt(r.rating, 10);
195				if (isNaN(rt) || rt < 1 || rt > 5) rt = 5;
196				starsEl.innerHTML = '<span class="mp-modal__stars-filled">' + '★'.repeat(rt) + '</span>' + '<span class="mp-modal__stars-empty">' + '★'.repeat(5 - rt) + '</span>';
197			}
198			var authorLine = r.author || '';
199			if(r.source === 'reference' && (r.company || r.city)) {
200				var meta = [r.company, r.city].filter(Boolean).join(' · ');
201				if(meta) authorLine += ' · ' + meta;
202			}
203			document.getElementById('mpModalAuthor').textContent = '— ' + authorLine;
204			var dateEl = document.getElementById('mpModalDate');
205			if (dateEl) {
206				dateEl.textContent = r.date_display || '';
207				dateEl.style.display = r.date_display ? 'block' : 'none';
208			}
209
210			var src = r.source || 'appstore';
211			var srcEl = document.getElementById('mpModalSource');
212			if(srcEl) srcEl.textContent = sourceLabels[src] || '';
213			if(srcEl) { srcEl.className = 'mp-modal__source mp-modal__source--' + src; }
214
215			var linkEl = document.getElementById('mpModalLink');
216			if(linkEl) {
217				linkEl.textContent = (sourceLinkLabels[src] || '') + ' →';
218				linkEl.href = r.source_url || sourceFallbackUrls[src] || '#';
219			}
220
221			var translatedEl = document.getElementById('mpModalTranslated');
222			var origLang = r.original_lang || '';
223			var hint = (origLang && origLang !== currentLang && langNames[currentLang]) ? (langNames[currentLang][origLang] || '') : '';
224			translatedEl.textContent = hint;
225			translatedEl.style.display = hint ? 'block' : 'none';
226			modal.classList.add('active');
227			document.body.style.overflow = 'hidden';
228		});
229	});
230
231	function closeModal() {
232		if (modal) { modal.classList.remove('active'); document.body.style.overflow = ''; }
233	}
234	var closeBtn = document.getElementById('mpModalCloseBtn');
235	var closeBg = document.getElementById('mpModalClose');
236	if (closeBtn) closeBtn.addEventListener('click', closeModal);
237	if (closeBg) closeBg.addEventListener('click', closeModal);
238	document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeModal(); });
239
240	// Hide submenu + secondary header
241	var sub = document.querySelector('.submenu');
242	if(sub) sub.style.display = 'none';
243	var secH = document.querySelector('.secondaryheader');
244	if(secH) secH.style.display = 'none';
245
246	// Industries Slider — infinite loop via first/last clone pattern.
247	// DOM layout after init:  [cloneLast, real0, real1, real2, real3, real4, cloneFirst]
248	// extIndex range 0..totalInd+1, currentExt stays within [1..totalInd] in rest state.
249	var indSlides = document.getElementById('mpIndustriesSlides');
250	var indDots = document.getElementById('mpIndDots');
251	var indPrev = document.getElementById('mpIndPrev');
252	var indNext = document.getElementById('mpIndNext');
253	var indRealSlides = document.querySelectorAll('.mp-ind-slide');
254
255	if(indSlides && indDots && indRealSlides.length) {
256		var totalInd = indRealSlides.length;
257		var firstClone = indRealSlides[0].cloneNode(true);
258		var lastClone = indRealSlides[totalInd - 1].cloneNode(true);
259		firstClone.setAttribute('aria-hidden', 'true');
260		lastClone.setAttribute('aria-hidden', 'true');
261		indSlides.appendChild(firstClone);
262		indSlides.insertBefore(lastClone, indRealSlides[0]);
263
264		var currentExt = 1; // 0 = lastClone, 1..totalInd = real, totalInd+1 = firstClone
265		var indAutoAdvance;
266		var isAnimating = false;
267
268		// Jump to initial position without transition
269		indSlides.style.transition = 'none';
270		indSlides.style.transform = 'translateX(-' + (currentExt * 100) + '%)';
271		void indSlides.offsetWidth;
272		indSlides.style.transition = '';
273
274		for(var i = 0; i < totalInd; i++) {
275			var dot = document.createElement('button');
276			dot.className = 'mp-ind-dot' + (i === 0 ? ' active' : '');
277			dot.setAttribute('data-index', i);
278			dot.addEventListener('click', function() {
279				goToExt(parseInt(this.getAttribute('data-index')) + 1);
280			});
281			indDots.appendChild(dot);
282		}
283
284		function updateDots() {
285			var realIdx = currentExt - 1;
286			if(realIdx < 0) realIdx = totalInd - 1;
287			if(realIdx >= totalInd) realIdx = 0;
288			document.querySelectorAll('.mp-ind-dot').forEach(function(d, i) { d.classList.toggle('active', i === realIdx); });
289		}
290
291		function goToExt(extIndex) {
292			if(isAnimating) return;
293			isAnimating = true;
294			currentExt = extIndex;
295			indSlides.style.transition = 'transform 0.6s ease';
296			indSlides.style.transform = 'translateX(-' + (extIndex * 100) + '%)';
297			updateDots();
298			resetIndAuto();
299		}
300
301		indSlides.addEventListener('transitionend', function(e) {
302			if(e.propertyName !== 'transform') return;
303			isAnimating = false;
304			if(currentExt === totalInd + 1) {
305				// Past last real — snap back to real first without animation
306				indSlides.style.transition = 'none';
307				currentExt = 1;
308				indSlides.style.transform = 'translateX(-' + (currentExt * 100) + '%)';
309				void indSlides.offsetWidth;
310				indSlides.style.transition = '';
311			} else if(currentExt === 0) {
312				// Before real first — snap to real last
313				indSlides.style.transition = 'none';
314				currentExt = totalInd;
315				indSlides.style.transform = 'translateX(-' + (currentExt * 100) + '%)';
316				void indSlides.offsetWidth;
317				indSlides.style.transition = '';
318			}
319		});
320
321		function resetIndAuto() {
322			clearInterval(indAutoAdvance);
323			indAutoAdvance = setInterval(function() { goToExt(currentExt + 1); }, 10000);
324		}
325
326		if(indPrev) indPrev.addEventListener('click', function() { goToExt(currentExt - 1); });
327		if(indNext) indNext.addEventListener('click', function() { goToExt(currentExt + 1); });
328		indAutoAdvance = setInterval(function() { goToExt(currentExt + 1); }, 5000);
329
330		var indSection = document.querySelector('.mp-industries-section');
331		if(indSection) {
332			indSection.addEventListener('mouseenter', function() { clearInterval(indAutoAdvance); });
333			indSection.addEventListener('mouseleave', function() { resetIndAuto(); });
334		}
335	}
336});

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.