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.