You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

497 lines
14 KiB

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: () => {
// Focus input field
animationElements[index].querySelector('input[type=text]')?.focus()
// Typewriter
const typewriterTexts = animationElements[index].querySelectorAll('.typewriter-text')
if (typewriterTexts.length > 0) {
const typewriter = typeWriterConsoleSequential(typewriterTexts[0].parentElement, {
baseSpeed: 10,
onAllComplete: () => {
if (index + 1 < animationElements.length) {
let nextAnimationElementInputField = animationElements[index+1].querySelector('input[type=text]')
if (nextAnimationElementInputField) {
animationElements[index].querySelectorAll('.typewriter-cursor').forEach((node) => {
node.classList.remove('typewriter-cursor')
})
}
}
// 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)
const targetHeights = []
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
let finalMinHeight = minHeight
if (bar.querySelector('.explanation')) {
finalMinHeight = Math.max(minHeight, 160)
}
targetHeights[i] = Math.max(calculatedHeight, finalMinHeight)
})
const percentageValues = percentages.map((val, idx) => ({ value: val, index: idx }))
const sortedPercentages = [...percentageValues].sort((a, b) => b.value - a.value)
const highestBar = sortedPercentages[0]
const lowestBar = sortedPercentages[sortedPercentages.length - 1]
let equalHeight = answerDistributions[0].offsetHeight
answerDistributions.forEach((bar, i) => {
let fakeHeight, finalHeight
if (i === highestBar.index) {
fakeHeight = containerHeight * 0.3
} else if (i === lowestBar.index) {
fakeHeight = containerHeight * 0.8
} else {
fakeHeight = containerHeight * 0.5
}
finalHeight = targetHeights[i]
gsap.to(bar, {
keyframes: [
{ height: fakeHeight, duration: 0.4, ease: "power2.out" },
{ height: equalHeight, duration: 0.4, ease: "power2.inOut" },
{ height: finalHeight, duration: 0.4, ease: "power2.out" }
],
onComplete: () => {
if (bar.dataset.answered != undefined) {
bar.classList.add('answered')
}
gsap.to(bar.querySelectorAll('.percentage'), {
opacity: 1,
duration: 0.4,
onComplete: 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 = 400,
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() + 100 // 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() * 15
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 }
}
}