parevo-interactive-video
Version:
Framework-agnostic interactive video library with quiz overlays for React, Vue, and vanilla JS
276 lines (268 loc) • 8.87 kB
JavaScript
class InteractiveVideo {
constructor(container, options) {
this.answeredQuestions = new Set();
this.currentQuestion = null;
this.isQuizVisible = false;
this.eventHandlers = new Map();
this.container = typeof container === 'string'
? document.querySelector(container)
: container;
if (!this.container) {
throw new Error('Container element not found');
}
this.options = {
width: 640,
height: 360,
autoplay: false,
muted: false,
controls: true,
className: '',
...options
};
this.init();
}
init() {
this.createVideoElement();
this.createOverlay();
this.setupEventListeners();
this.applyStyles();
}
createVideoElement() {
this.video = document.createElement('video');
this.video.src = this.options.src;
this.video.width = this.options.width;
this.video.height = this.options.height;
this.video.controls = this.options.controls;
this.video.muted = this.options.muted;
this.video.autoplay = this.options.autoplay;
this.video.preload = 'metadata';
// Video elementinin kaybolmaması için style
this.video.style.display = 'block';
this.video.style.width = '100%';
this.video.style.height = '100%';
if (this.options.className) {
this.video.className = this.options.className;
}
this.container.appendChild(this.video);
}
createOverlay() {
this.overlay = document.createElement('div');
this.overlay.className = 'interactive-video-quiz-overlay';
this.overlay.style.display = 'none';
this.container.appendChild(this.overlay);
}
setupEventListeners() {
this.video.addEventListener('timeupdate', this.handleTimeUpdate.bind(this));
this.video.addEventListener('ended', this.handleVideoEnd.bind(this));
this.video.addEventListener('error', this.handleError.bind(this));
}
handleTimeUpdate() {
const currentTime = this.video.currentTime;
this.checkForQuestions(currentTime);
}
checkForQuestions(currentTime) {
if (this.isQuizVisible)
return;
const triggerQuestion = this.options.config.questions.find(q => !this.answeredQuestions.has(q.id) &&
currentTime >= q.triggerTime &&
currentTime < q.triggerTime + 1);
if (triggerQuestion) {
this.showQuiz(triggerQuestion);
}
}
showQuiz(question) {
this.currentQuestion = question;
this.isQuizVisible = true;
this.video.pause();
this.overlay.innerHTML = `
<div class="quiz-content">
<h3 class="quiz-question">${question.question}</h3>
<div class="quiz-options">
${question.options.map(option => `
<button class="quiz-option" data-option-id="${option.id}">
${option.text}
</button>
`).join('')}
</div>
</div>
`;
this.overlay.style.display = 'flex';
// Option click handler - delegated event
this.overlay.onclick = (event) => this.handleOptionClick(event);
}
handleOptionClick(event) {
const target = event.target;
if (!target.classList.contains('quiz-option'))
return;
const optionId = target.dataset.optionId;
this.handleAnswer(optionId);
}
handleAnswer(optionId) {
if (!this.currentQuestion)
return;
const selectedOption = this.currentQuestion.options.find(opt => opt.id === optionId);
if (!selectedOption)
return;
const isCorrect = selectedOption.isCorrect;
this.video.currentTime;
const questionData = this.currentQuestion; // Store before clearing
// Sadece doğru cevap verildiğinde answered listesine ekle
if (isCorrect) {
this.answeredQuestions.add(questionData.id);
}
// Quiz'i hemen gizle
this.hideQuiz();
// Callback
this.options.onQuestionAnswered?.(questionData.id, optionId, isCorrect);
this.emit('questionAnswered', { questionId: questionData.id, optionId, isCorrect });
// Video control
if (isCorrect) {
this.video.play();
}
else {
// Video'yu pause et
this.video.pause();
// Rewind işlemi
const rewindTo = questionData.rewindTime;
this.video.currentTime = rewindTo;
// Video'yu tekrar başlat
setTimeout(() => {
this.video.play().catch(error => {
console.warn('Video play failed:', error);
});
}, 50);
}
this.currentQuestion = null;
}
hideQuiz() {
this.isQuizVisible = false;
this.overlay.style.display = 'none';
this.overlay.innerHTML = ''; // Sadece overlay içeriğini temizle
this.overlay.onclick = null; // Click handler'ı temizle
// Video elementinin DOM'da olup olmadığını kontrol et ve gerekirse geri ekle
if (!document.contains(this.video)) {
this.container.appendChild(this.video);
}
}
handleVideoEnd() {
this.options.onVideoEnd?.();
this.emit('videoEnd');
}
handleError(event) {
const error = new Error('Video playback error');
this.options.onError?.(error);
this.emit('error', error);
}
applyStyles() {
// Container styles
this.container.style.position = 'relative';
this.container.style.display = 'inline-block';
this.container.style.overflow = 'hidden';
// Default overlay styles
const style = document.createElement('style');
style.textContent = `
.interactive-video-quiz-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.quiz-content {
background-color: white;
padding: 2rem;
border-radius: 8px;
max-width: 80%;
max-height: 80%;
overflow-y: auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.quiz-question {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1.5rem;
color: #333;
line-height: 1.4;
}
.quiz-options {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.quiz-option {
padding: 0.75rem 1rem;
background-color: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s ease;
text-align: left;
}
.quiz-option:hover {
background-color: #007bff;
border-color: #007bff;
color: white;
}
.quiz-option:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}
`;
if (!document.querySelector('#interactive-video-styles')) {
style.id = 'interactive-video-styles';
document.head.appendChild(style);
}
}
// Public API methods
play() {
this.video.play();
}
pause() {
this.video.pause();
}
currentTime(time) {
if (time !== undefined) {
this.video.currentTime = time;
}
else {
return this.video.currentTime;
}
}
destroy() {
this.video.removeEventListener('timeupdate', this.handleTimeUpdate.bind(this));
this.video.removeEventListener('ended', this.handleVideoEnd.bind(this));
this.video.removeEventListener('error', this.handleError.bind(this));
this.container.removeChild(this.video);
this.container.removeChild(this.overlay);
this.eventHandlers.clear();
}
on(event, callback) {
if (!this.eventHandlers.has(event)) {
this.eventHandlers.set(event, []);
}
this.eventHandlers.get(event).push(callback);
}
off(event, callback) {
const handlers = this.eventHandlers.get(event);
if (handlers) {
const index = handlers.indexOf(callback);
if (index > -1) {
handlers.splice(index, 1);
}
}
}
emit(event, data) {
const handlers = this.eventHandlers.get(event);
if (handlers) {
handlers.forEach(handler => handler(data));
}
}
}
export { InteractiveVideo };
//# sourceMappingURL=index.js.map