import "@hotwired/turbo-rails"
|
|
import { Application } from "@hotwired/stimulus"
|
|
|
|
import LocaleController from "locale_controller"
|
|
import ImageController from "image_controller"
|
|
|
|
const application = Application.start()
|
|
|
|
application.register("locale", LocaleController)
|
|
application.register("image", ImageController)
|
|
|
|
// Configure Stimulus development experience
|
|
application.debug = false
|
|
window.Stimulus = application
|
|
|
|
export { application }
|
|
|
|
var animationElements
|
|
var originalHeights
|
|
|
|
|
|
document.addEventListener('turbo:render', (event) => {
|
|
console.info('turbo:render')
|
|
|
|
let errorElements = document.querySelectorAll('.field_with_errors')
|
|
|
|
if (errorElements.length > 0) {
|
|
animationElements = document.querySelectorAll('.animation-element');
|
|
gsap.set(animationElements, {
|
|
opacity: 1
|
|
})
|
|
}
|
|
});
|
|
|
|
|
|
// document.addEventListener("turbo:before-stream-render", animateElements)
|
|
document.addEventListener("turbo:load", animateElements)
|
|
|
|
|
|
function animateElements(event) {
|
|
|
|
console.info(event.type)
|
|
|
|
// Copy link to clipboard
|
|
document.querySelectorAll('[data-share-title]').forEach((share_btn) => {
|
|
share_btn.addEventListener('click', (e) => {
|
|
|
|
if (navigator.share) {
|
|
navigator.share({
|
|
title: e.currentTarget.getAttribute('data-share-title'),
|
|
text: e.currentTarget.getAttribute('data-share-text'),
|
|
url: e.currentTarget.getAttribute('data-share-url'),
|
|
}).then(() => {
|
|
console.log('Thanks for sharing!');
|
|
})
|
|
.catch(console.error);
|
|
} else {
|
|
navigator.clipboard.writeText(window.location.href)
|
|
alert('URL copied to clipboard!')
|
|
}
|
|
})
|
|
})
|
|
|
|
originalHeights = []
|
|
animationElements = document.querySelectorAll('.animation-element');
|
|
|
|
animationElements.forEach((animationElement, index) => {
|
|
const height = animationElement.offsetHeight
|
|
originalHeights.push(height)
|
|
// console.log(`Element ${index + 1} height: ${height}px`)
|
|
})
|
|
|
|
gsap.set(animationElements, {
|
|
height: 0,
|
|
opacity: 0,
|
|
overflow: 'hidden'
|
|
})
|
|
|
|
let typewriterElements = document.querySelectorAll('.typewriter-text');
|
|
if (typewriterElements.length > 0) {
|
|
gsap.set(typewriterElements, {
|
|
opacity: 0
|
|
})
|
|
}
|
|
|
|
// Start the sequential animation
|
|
animateElementsSequentially();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function animateElementsSequentially(index = 0) {
|
|
|
|
if (index >= animationElements.length) {
|
|
console.log('All animations complete');
|
|
return;
|
|
}
|
|
|
|
// 'margin-top': (index == (animationElements.length-1) ? "12px" : "8px"),
|
|
|
|
gsap.to(animationElements[index], {
|
|
height: originalHeights[index],
|
|
opacity: 1,
|
|
duration: (0.004 * originalHeights[index]),
|
|
delay: (index > 0 ? 0.4 : 0),
|
|
ease: "power2.out",
|
|
onComplete: () => {
|
|
|
|
// Typewriter
|
|
const typewriterTexts = animationElements[index].querySelectorAll('.typewriter-text')
|
|
if (typewriterTexts.length > 0) {
|
|
const typewriter = typeWriterConsoleSequential(typewriterTexts[0].parentElement, {
|
|
baseSpeed: 20,
|
|
onAllComplete: () => {
|
|
// Start next element after typewriter finishes
|
|
animateElementsSequentially(index + 1);
|
|
}
|
|
})
|
|
typewriter.start()
|
|
return
|
|
}
|
|
|
|
// % Answer Distributions
|
|
const answerDistributions = animationElements[index].querySelectorAll('.answer-distribution')
|
|
if (answerDistributions.length > 0 ) {
|
|
|
|
const containerHeight = animationElements[index].offsetHeight - 8; // - padding
|
|
|
|
const percentages = Array.from(answerDistributions).map(bar => parseInt(bar.dataset.value));
|
|
const maxPercentage = Math.max(...percentages);
|
|
|
|
console.info(maxPercentage)
|
|
|
|
answerDistributions.forEach((bar, i) => {
|
|
const percentage = parseInt(bar.dataset.value);
|
|
const relativePercentage = (percentage / maxPercentage) * 100;
|
|
const calculatedHeight = (relativePercentage / 100) * containerHeight;
|
|
|
|
|
|
const computedStyle = window.getComputedStyle(bar);
|
|
const minHeight = parseInt(computedStyle.minHeight) || 0;
|
|
|
|
const targetHeight = Math.max(calculatedHeight, minHeight);
|
|
|
|
gsap.to(bar, {
|
|
height: targetHeight,
|
|
duration: 0.4,
|
|
ease: "power2.out",
|
|
delay: index * 0.1,
|
|
onAllComplete: () => {
|
|
|
|
if (bar.dataset.answered != undefined) {
|
|
bar.classList.add('answered')
|
|
}
|
|
|
|
gsap.to(bar.querySelectorAll('.percentage'), {
|
|
opacity: 1,
|
|
duration: 0.4,
|
|
onAllComplete: function() {
|
|
const explanationElement = this.targets()[0].nextElementSibling
|
|
if (explanationElement) {
|
|
explanationElement.style.display = 'block'
|
|
}
|
|
}
|
|
})
|
|
|
|
|
|
if (i == (answerDistributions.length - 1)) {
|
|
setTimeout(() => animateElementsSequentially(index + 1), 1000);
|
|
|
|
}
|
|
}
|
|
});
|
|
|
|
})
|
|
return
|
|
}
|
|
|
|
|
|
animateElementsSequentially(index + 1);
|
|
|
|
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function typeWriterConsoleSequential(container, options = {}) {
|
|
const {
|
|
selector = '.typewriter-text',
|
|
baseSpeed = 60,
|
|
divDelay = 600,
|
|
showCursor = true,
|
|
onDivComplete = null,
|
|
onAllComplete = null
|
|
} = options;
|
|
|
|
const textDivs = container.querySelectorAll(selector);
|
|
if (textDivs.length === 0) return null;
|
|
|
|
let currentDivIndex = 0;
|
|
let isActive = false;
|
|
let animationId = null;
|
|
let timeoutIds = new Set(); // Track all timeouts for cleanup
|
|
|
|
// Cleanup function for Turbo navigation
|
|
function cleanup() {
|
|
isActive = false;
|
|
if (animationId) {
|
|
cancelAnimationFrame(animationId);
|
|
animationId = null;
|
|
}
|
|
// Clear all timeouts
|
|
timeoutIds.forEach(id => clearTimeout(id));
|
|
timeoutIds.clear();
|
|
}
|
|
|
|
// Set up Turbo event listeners for cleanup
|
|
function setupTurboListeners() {
|
|
const turboEvents = ['turbo:before-visit', 'turbo:before-cache', 'beforeunload'];
|
|
|
|
turboEvents.forEach(eventName => {
|
|
document.addEventListener(eventName, cleanup, { once: false });
|
|
});
|
|
|
|
// Store cleanup function on container for manual cleanup
|
|
container._typewriterCleanup = cleanup;
|
|
}
|
|
|
|
// Enhanced setTimeout that tracks IDs
|
|
function safeSetTimeout(callback, delay) {
|
|
const id = setTimeout(() => {
|
|
timeoutIds.delete(id);
|
|
if (isActive) callback();
|
|
}, delay);
|
|
timeoutIds.add(id);
|
|
return id;
|
|
}
|
|
|
|
// Store original text and prepare divs
|
|
function initializeDivs() {
|
|
// Set container height to prevent layout shifts
|
|
const containerHeight = container.offsetHeight;
|
|
container.style.minHeight = `${containerHeight}px`;
|
|
|
|
textDivs.forEach(div => {
|
|
if (!div.dataset.originalText) {
|
|
div.dataset.originalText = div.textContent.trim();
|
|
}
|
|
// Use visibility instead of blanking content to maintain layout
|
|
div.style.visibility = 'hidden';
|
|
div.innerHTML = '';
|
|
div.style.opacity = '1';
|
|
});
|
|
|
|
// Force reflow to ensure styles are applied
|
|
container.offsetHeight;
|
|
}
|
|
|
|
function createCursorSpan() {
|
|
// No longer needed - cursor is now handled via CSS
|
|
return null;
|
|
}
|
|
|
|
function updateDivContent(div, text, withCursor = false) {
|
|
// Use textContent for better performance
|
|
div.textContent = text;
|
|
|
|
// Toggle cursor class instead of adding DOM element
|
|
if (withCursor && showCursor) {
|
|
div.classList.add('typewriter-cursor');
|
|
} else {
|
|
div.classList.remove('typewriter-cursor');
|
|
}
|
|
}
|
|
|
|
function animateNextDiv() {
|
|
if (currentDivIndex >= textDivs.length || !isActive) {
|
|
isActive = false;
|
|
if (onAllComplete) {
|
|
// Use setTimeout to avoid potential timing issues
|
|
requestAnimationFrame(() => {
|
|
safeSetTimeout(() => onAllComplete(container), 200);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
const currentDiv = textDivs[currentDivIndex];
|
|
const originalText = currentDiv.dataset.originalText;
|
|
const isLastDiv = currentDivIndex === textDivs.length - 1;
|
|
|
|
// Make div visible and prepare for typing
|
|
currentDiv.style.visibility = 'visible';
|
|
updateDivContent(currentDiv, '', showCursor);
|
|
currentDiv.classList.add('typing-active');
|
|
|
|
let charIndex = 0;
|
|
let nextCharTime = performance.now() + 200; // Initial delay
|
|
|
|
function typeFrame(currentTime) {
|
|
if (!isActive) {
|
|
if (animationId) {
|
|
cancelAnimationFrame(animationId);
|
|
animationId = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (currentTime >= nextCharTime && charIndex < originalText.length) {
|
|
charIndex++;
|
|
const currentText = originalText.substring(0, charIndex);
|
|
updateDivContent(currentDiv, currentText, true);
|
|
|
|
// Calculate delay for next character
|
|
let delay = baseSpeed + Math.random() * 30;
|
|
const char = originalText.charAt(charIndex - 1);
|
|
|
|
if (char === '.' || char === '!' || char === '?') {
|
|
delay += 250;
|
|
} else if (char === ',' || char === ';') {
|
|
delay += 120;
|
|
} else if (char === ' ') {
|
|
delay += 60;
|
|
}
|
|
|
|
nextCharTime = currentTime + delay;
|
|
}
|
|
|
|
// Check if current div is complete
|
|
if (charIndex >= originalText.length) {
|
|
currentDiv.classList.remove('typing-active');
|
|
|
|
// Final content update
|
|
updateDivContent(currentDiv, originalText, showCursor);
|
|
|
|
if (onDivComplete) {
|
|
onDivComplete(currentDiv, currentDivIndex, container);
|
|
}
|
|
|
|
currentDivIndex++;
|
|
|
|
if (isActive && currentDivIndex < textDivs.length) {
|
|
// Move to next div after delay
|
|
safeSetTimeout(() => {
|
|
if (isActive) {
|
|
// Remove cursor from previous div (except last one)
|
|
if (!isLastDiv) {
|
|
updateDivContent(currentDiv, originalText, false);
|
|
}
|
|
animateNextDiv();
|
|
}
|
|
}, divDelay);
|
|
} else if (isActive) {
|
|
// This was the last div
|
|
safeSetTimeout(() => {
|
|
if (onAllComplete) {
|
|
onAllComplete(container);
|
|
}
|
|
}, 200);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Continue animation
|
|
if (isActive) {
|
|
animationId = requestAnimationFrame(typeFrame);
|
|
}
|
|
}
|
|
|
|
// Start the animation
|
|
animationId = requestAnimationFrame(typeFrame);
|
|
}
|
|
|
|
// Initialize Turbo listeners
|
|
setupTurboListeners();
|
|
|
|
// Add required CSS for cursor pseudo-element
|
|
function addCursorStyles() {
|
|
const styleId = 'typewriter-cursor-styles';
|
|
if (!document.getElementById(styleId)) {
|
|
const style = document.createElement('style');
|
|
style.id = styleId;
|
|
style.textContent = `
|
|
.typewriter-cursor::after {
|
|
content: '|';
|
|
animation: typewriter-blink 1s infinite;
|
|
color: currentColor;
|
|
}
|
|
|
|
@keyframes typewriter-blink {
|
|
0%, 50% { opacity: 1; }
|
|
51%, 100% { opacity: 0; }
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
}
|
|
|
|
// Initialize styles
|
|
addCursorStyles();
|
|
|
|
return {
|
|
start: () => {
|
|
if (isActive) return; // Prevent multiple starts
|
|
|
|
initializeDivs();
|
|
isActive = true;
|
|
currentDivIndex = 0;
|
|
|
|
// Use requestAnimationFrame for smoother start
|
|
requestAnimationFrame(() => {
|
|
if (isActive) {
|
|
animateNextDiv();
|
|
}
|
|
});
|
|
},
|
|
|
|
stop: () => {
|
|
cleanup();
|
|
},
|
|
|
|
reset: () => {
|
|
cleanup();
|
|
|
|
textDivs.forEach(div => {
|
|
if (div.dataset.originalText) {
|
|
div.style.visibility = 'visible';
|
|
updateDivContent(div, div.dataset.originalText, false);
|
|
div.classList.remove('typing-active');
|
|
div.classList.remove('typewriter-cursor');
|
|
}
|
|
});
|
|
currentDivIndex = 0;
|
|
},
|
|
|
|
// Manual cleanup method for explicit cleanup
|
|
destroy: () => {
|
|
cleanup();
|
|
// Remove Turbo event listeners
|
|
const turboEvents = ['turbo:before-visit', 'turbo:before-cache', 'beforeunload'];
|
|
turboEvents.forEach(eventName => {
|
|
document.removeEventListener(eventName, cleanup);
|
|
});
|
|
// Remove cleanup reference
|
|
delete container._typewriterCleanup;
|
|
},
|
|
|
|
get isActive() { return isActive; }
|
|
};
|
|
}
|