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