quad-tap
Version:
A pure JavaScript implementation of the Quad-Tap overlay interaction for videos with advanced video player API integration
484 lines (418 loc) • 12.9 kB
JavaScript
/**
* UnifiedControlStrip Component
* A unified control strip that can be used in both overlay and lightbox contexts
*/
import { createElement } from '../helpers/dom';
import { layoutControlStrip } from '../helpers/layout';
/**
* Create a unified control strip
* @param {Object} options - Configuration options
* @param {HTMLElement} options.container - The container element
* @param {HTMLElement} options.video - The video element
* @param {number} options.rewindTime - Time in seconds to rewind
* @param {number} options.forwardTime - Time in seconds to forward
* @param {boolean} options.debug - Enable debug logging
* @returns {Object} The control strip object with element and methods
*/
export function createUnifiedControlStrip(options) {
const {
container,
video,
rewindTime = 30,
forwardTime = 30,
debug = false
} = options;
// Log function for debugging
const log = (message, data) => {
if (debug) {
console.log(`[UnifiedControlStrip] ${message}`, data || '');
}
};
log('Creating unified control strip', options);
// Create control strip container
const controlStrip = createElement('div', {
className: 'qt-control-strip',
styles: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '10px',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderRadius: '4px',
color: 'white'
}
});
// Create time display
const timeDisplay = createElement('div', {
className: 'qt-time-display',
styles: {
fontSize: '14px',
marginRight: '10px',
minWidth: '80px',
textAlign: 'center'
}
});
// Create buttons container
const buttonsContainer = createElement('div', {
className: 'qt-buttons-container',
styles: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flex: '1'
}
});
// Check if we're on a mobile device
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) || window.innerWidth < 768;
// Check orientation - true if portrait (height > width)
const isPortrait = window.innerHeight > window.innerWidth;
// Calculate responsive font size based on device and orientation
const getResponsiveButtonStyles = (isLarge = false) => {
// Base font size
let fontSize = isLarge ? 24 : 20;
// Adjust for mobile
if (isMobile) {
fontSize = isLarge ? 22 : 18;
// Further adjust for portrait orientation
if (isPortrait) {
fontSize = isLarge ? 20 : 16;
}
}
return {
backgroundColor: 'transparent',
border: 'none',
color: 'white',
fontSize: `${fontSize}px`,
cursor: 'pointer',
padding: '5px 10px',
margin: '0 5px',
transition: 'transform 0.2s ease, font-size 0.3s ease'
};
};
// Create rewind button
const rewindButton = createElement('button', {
className: 'qt-control-button qt-rewind-button',
text: '⏪',
attributes: {
'data-cmd': 'seek:-30',
'title': `Rewind ${rewindTime} seconds`
},
styles: getResponsiveButtonStyles()
});
// Create play/pause button
const playPauseButton = createElement('button', {
className: 'qt-control-button qt-play-pause-button',
text: '▶️',
attributes: {
'data-cmd': 'play',
'title': 'Play/Pause'
},
styles: getResponsiveButtonStyles(true) // Larger size for play/pause
});
// Create forward button
const forwardButton = createElement('button', {
className: 'qt-control-button qt-forward-button',
text: '⏩',
attributes: {
'data-cmd': 'seek:+30',
'title': `Forward ${forwardTime} seconds`
},
styles: getResponsiveButtonStyles()
});
// Create share button
const shareButton = createElement('button', {
className: 'qt-control-button qt-share-button',
text: '📤',
attributes: {
'data-cmd': 'share',
'title': 'Share'
},
styles: getResponsiveButtonStyles()
});
// Create copy URL button
const copyUrlButton = createElement('button', {
className: 'qt-control-button qt-copy-url-button',
text: '🔗',
attributes: {
'data-cmd': 'copyurl',
'title': 'Copy URL'
},
styles: getResponsiveButtonStyles()
});
// Create slider container
const sliderContainer = createElement('div', {
className: 'qt-slider-container',
styles: {
width: '100%',
padding: '10px 0',
display: 'flex',
flexDirection: 'column'
}
});
// Create slider
const slider = createElement('input', {
className: 'qt-video-slider',
attributes: {
'type': 'range',
'min': '0',
'max': '100',
'value': '0',
'step': '0.1'
},
styles: {
width: '100%',
margin: '5px 0',
cursor: 'pointer'
}
});
// Add buttons to container
buttonsContainer.appendChild(rewindButton);
buttonsContainer.appendChild(playPauseButton);
buttonsContainer.appendChild(forwardButton);
buttonsContainer.appendChild(shareButton);
buttonsContainer.appendChild(copyUrlButton);
// Add elements to control strip
controlStrip.appendChild(timeDisplay);
controlStrip.appendChild(buttonsContainer);
// Store references to elements
const elements = {
controlStrip,
timeDisplay,
buttonsContainer,
rewindButton,
playPauseButton,
forwardButton,
shareButton,
copyUrlButton,
sliderContainer,
slider
};
// Event handlers
let eventHandlers = {};
// Bind events to control strip buttons
function bindEvents() {
log('Binding events');
// Unbind any existing events first
unbindEvents();
// Play/pause button
eventHandlers.playPause = () => {
log('Play/pause clicked');
if (video) {
if (video.paused) {
video.play()
.then(() => {
updatePlayPauseButton(true);
})
.catch(err => {
log('Error playing video', err);
});
} else {
video.pause();
updatePlayPauseButton(false);
}
}
};
playPauseButton.addEventListener('click', eventHandlers.playPause);
// Rewind button
eventHandlers.rewind = () => {
log('Rewind clicked');
if (video) {
video.currentTime = Math.max(0, video.currentTime - rewindTime);
updateTimeDisplay();
}
};
rewindButton.addEventListener('click', eventHandlers.rewind);
// Forward button
eventHandlers.forward = () => {
log('Forward clicked');
if (video) {
video.currentTime = Math.min(video.duration, video.currentTime + forwardTime);
updateTimeDisplay();
}
};
forwardButton.addEventListener('click', eventHandlers.forward);
// Share button
eventHandlers.share = () => {
log('Share clicked');
if (navigator.share) {
navigator.share({
title: 'Shared Video',
text: 'Check out this video!',
url: window.location.href
}).catch(err => {
log('Share failed', err);
});
} else {
alert('Share feature not supported by your browser');
}
};
shareButton.addEventListener('click', eventHandlers.share);
// Copy URL button
eventHandlers.copyUrl = () => {
log('Copy URL clicked');
const url = window.location.href;
navigator.clipboard.writeText(url)
.then(() => {
alert('URL copied to clipboard');
})
.catch(err => {
log('Copy failed', err);
// Fallback
const input = document.createElement('input');
input.value = url;
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
alert('URL copied to clipboard');
});
};
copyUrlButton.addEventListener('click', eventHandlers.copyUrl);
// Slider input
eventHandlers.sliderInput = (evt) => {
if (video) {
const seekTime = (evt.target.value / 100) * video.duration;
video.currentTime = seekTime;
updateTimeDisplay();
}
};
slider.addEventListener('input', eventHandlers.sliderInput);
// Video timeupdate event
eventHandlers.timeUpdate = () => {
updateTimeDisplay();
};
if (video) {
video.addEventListener('timeupdate', eventHandlers.timeUpdate);
}
// Video play event
eventHandlers.videoPlay = () => {
updatePlayPauseButton(true);
};
if (video) {
video.addEventListener('play', eventHandlers.videoPlay);
}
// Video pause event
eventHandlers.videoPause = () => {
updatePlayPauseButton(false);
};
if (video) {
video.addEventListener('pause', eventHandlers.videoPause);
}
}
// Unbind events
function unbindEvents() {
log('Unbinding events');
if (eventHandlers.playPause) {
playPauseButton.removeEventListener('click', eventHandlers.playPause);
}
if (eventHandlers.rewind) {
rewindButton.removeEventListener('click', eventHandlers.rewind);
}
if (eventHandlers.forward) {
forwardButton.removeEventListener('click', eventHandlers.forward);
}
if (eventHandlers.share) {
shareButton.removeEventListener('click', eventHandlers.share);
}
if (eventHandlers.copyUrl) {
copyUrlButton.removeEventListener('click', eventHandlers.copyUrl);
}
if (eventHandlers.sliderInput) {
slider.removeEventListener('input', eventHandlers.sliderInput);
}
if (video && eventHandlers.timeUpdate) {
video.removeEventListener('timeupdate', eventHandlers.timeUpdate);
}
if (video && eventHandlers.videoPlay) {
video.removeEventListener('play', eventHandlers.videoPlay);
}
if (video && eventHandlers.videoPause) {
video.removeEventListener('pause', eventHandlers.videoPause);
}
// Reset event handlers
eventHandlers = {};
}
// Update play/pause button state
function updatePlayPauseButton(isPlaying) {
if (isPlaying) {
playPauseButton.textContent = '⏸️';
playPauseButton.setAttribute('title', 'Pause');
} else {
playPauseButton.textContent = '▶️';
playPauseButton.setAttribute('title', 'Play');
}
}
// Update time display
function updateTimeDisplay() {
if (!video) return;
// Format time
const formatTime = (seconds) => {
if (isNaN(seconds) || !isFinite(seconds)) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
};
const currentTime = formatTime(video.currentTime);
const duration = formatTime(video.duration);
// Update time display
timeDisplay.textContent = `${currentTime} / ${duration}`;
// Update slider position
if (video.duration) {
const percent = (video.currentTime / video.duration) * 100;
slider.value = percent;
}
}
// Set mode (overlay or lightbox)
function setMode(mode, container) {
log(`Setting mode to ${mode}`);
// Apply layout based on mode
layoutControlStrip(controlStrip, mode, container);
// Show/hide elements based on mode
if (mode === 'overlay') {
// In overlay mode, hide slider and some buttons
if (sliderContainer.parentNode === controlStrip) {
controlStrip.removeChild(sliderContainer);
}
// Hide share and copy URL buttons in overlay mode
shareButton.style.display = 'none';
copyUrlButton.style.display = 'none';
} else if (mode === 'lightbox') {
// In lightbox mode, show slider and all buttons
if (sliderContainer.parentNode !== controlStrip) {
// Add slider before buttons
controlStrip.insertBefore(sliderContainer, buttonsContainer);
}
// Show share and copy URL buttons in lightbox mode
shareButton.style.display = 'block';
copyUrlButton.style.display = 'block';
}
// Update time display
updateTimeDisplay();
// Update play/pause button state
if (video) {
updatePlayPauseButton(!video.paused);
}
}
// Initialize
function init() {
log('Initializing');
// Set initial state
updateTimeDisplay();
if (video) {
updatePlayPauseButton(!video.paused);
}
// Bind events
bindEvents();
}
// Initialize
init();
// Return public API
return {
element: controlStrip,
bindEvents,
unbindEvents,
setMode,
updateTimeDisplay,
updatePlayPauseButton
};
}