UNPKG

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
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