Skip to content

Commit 7d124c1

Browse files
committed
fix: mobile touch event
1 parent 2c5b4ed commit 7d124c1

File tree

2 files changed

+236
-0
lines changed

2 files changed

+236
-0
lines changed

src/assets/styles/main.scss

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ $icon-font-path: '../icon-font' !default;
193193
visibility: visible;
194194
}
195195

196+
196197
&.vjs-error .vjs-error-display {
197198
background: #90a0b3;
198199

@@ -232,6 +233,43 @@ $icon-font-path: '../icon-font' !default;
232233
opacity: 0;
233234
}
234235

236+
// Mobile touch interaction - show big play button when tapped (must come after hiding rules above)
237+
&.cld-mobile-touch-active .vjs-big-play-button {
238+
display: block !important;
239+
opacity: 1 !important;
240+
visibility: visible !important;
241+
}
242+
243+
// Mobile touch - ONLY style the pause icon during mobile touch interactions
244+
&.cld-mobile-touch-playing .vjs-big-play-button .vjs-icon-placeholder:before {
245+
// Reset the CSS triangle properties completely
246+
border: none !important;
247+
border-left: none !important;
248+
border-top: none !important;
249+
border-bottom: none !important;
250+
margin: 0 !important;
251+
252+
// Create perfectly centered pause icon with CSS
253+
content: '' !important;
254+
position: absolute !important;
255+
256+
// Perfect mathematical centering for both lines
257+
top: 50% !important;
258+
left: 50% !important;
259+
transform: translate(-50%, -50%) translate(-5.5px, 0) !important; // Center the 11px total width (4px + 3px + 4px)
260+
261+
// Identical pause lines - clean and proportioned
262+
width: 4px !important;
263+
height: 22px !important;
264+
background: currentColor !important;
265+
border-radius: 2px !important;
266+
267+
// Create identical second line with perfect 3px spacing
268+
box-shadow: 7px 0 0 0 currentColor !important; // 4px line + 3px gap = 7px offset
269+
270+
display: block !important;
271+
}
272+
235273
&.vjs-controls-enabled::before {
236274
content: '';
237275
pointer-events: none;

src/video-player.js

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Object.keys(plugins).forEach((key) => {
3636

3737
overrideDefaultVideojsComponents();
3838

39+
3940
class VideoPlayer extends Utils.mixin(Eventable) {
4041

4142
static all(selector, ...args) {
@@ -93,6 +94,7 @@ class VideoPlayer extends Utils.mixin(Eventable) {
9394
this._initPlugins();
9495
this._initJumpButtons();
9596
this._initPictureInPicture();
97+
this._initMobileTouchHandler();
9698
this._setVideoJsListeners(ready);
9799
}
98100

@@ -406,6 +408,202 @@ class VideoPlayer extends Utils.mixin(Eventable) {
406408
}
407409
}
408410

411+
_initMobileTouchHandler() {
412+
// Check if this is a mobile device
413+
this._isMobile = videojs.browser.IS_IOS || videojs.browser.IS_ANDROID ||
414+
/Mobi|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
415+
416+
if (!this._isMobile) {
417+
return;
418+
}
419+
420+
if (this._mobileTouchHandlerSetup) {
421+
return;
422+
}
423+
424+
// Initialize mobile interaction tracking
425+
this._mobileInteractionActive = false;
426+
427+
const setupTouchHandler = () => {
428+
const playerElement = this.videojs.el();
429+
430+
if (!playerElement) {
431+
setTimeout(setupTouchHandler, 100);
432+
return;
433+
}
434+
435+
// Add touch event listener for mobile devices
436+
const handleMobileTouch = (event) => {
437+
// Only handle touch events on the player container, not on controls
438+
const target = event.target;
439+
const isControlElement = target.closest('.vjs-control-bar') ||
440+
target.closest('.vjs-menu') ||
441+
target.matches('.vjs-control, .vjs-button');
442+
443+
if (isControlElement) {
444+
return;
445+
}
446+
447+
// Check if video has started (show big play button for both playing and paused states)
448+
const hasStarted = this.videojs.hasStarted();
449+
450+
if (hasStarted) {
451+
// Show the touch overlay and mark interaction as active
452+
this.videojs.addClass('cld-mobile-touch-active');
453+
this._mobileInteractionActive = true;
454+
455+
// Update mobile touch state based on current playback
456+
this._updateMobileTouchState();
457+
458+
// Remove the overlay after timeout
459+
this.videojs.clearTimeout(this._mobileTouchTimeout);
460+
this._mobileTouchTimeout = this.videojs.setTimeout(() => {
461+
// First remove the visibility class to start fade-out
462+
this.videojs.removeClass('cld-mobile-touch-active');
463+
464+
// Wait for CSS transition to complete before removing pause icon class
465+
setTimeout(() => {
466+
this.videojs.removeClass('cld-mobile-touch-playing');
467+
this._removeMobileBigPlayButtonHandler();
468+
this._mobileInteractionActive = false; // End mobile interaction session
469+
}, 250); // Wait slightly longer than the 0.2s CSS transition
470+
}, 3000);
471+
}
472+
};
473+
474+
playerElement.addEventListener('touchend', handleMobileTouch, { passive: true });
475+
476+
// Listen to play/pause events to update mobile touch state (only during active mobile interactions)
477+
this.videojs.on('play', () => {
478+
if (this._isMobile && this._mobileInteractionActive) {
479+
this._updateMobileTouchState();
480+
}
481+
});
482+
483+
this.videojs.on('pause', () => {
484+
if (this._isMobile && this._mobileInteractionActive) {
485+
this._updateMobileTouchState();
486+
}
487+
});
488+
489+
// Mark as setup complete
490+
this._mobileTouchHandlerSetup = true;
491+
492+
// Clean up on dispose
493+
this.videojs.on('dispose', () => {
494+
playerElement.removeEventListener('touchend', handleMobileTouch);
495+
this.videojs.clearTimeout(this._mobileTouchTimeout);
496+
this._removeMobileBigPlayButtonHandler();
497+
this._mobileTouchHandlerSetup = false;
498+
this._mobileInteractionActive = false;
499+
});
500+
};
501+
502+
// Initialize when VideoJS is ready
503+
if (this.videojs && this.videojs.isReady_) {
504+
setupTouchHandler();
505+
} else if (this.videojs) {
506+
this.videojs.ready(setupTouchHandler);
507+
} else {
508+
setTimeout(() => this._initMobileTouchHandler(), 200);
509+
}
510+
}
511+
512+
_updateMobileTouchState() {
513+
const isPlaying = !this.videojs.paused();
514+
515+
if (isPlaying) {
516+
this.videojs.addClass('cld-mobile-touch-playing');
517+
this._setupMobileBigPlayButtonHandler(true);
518+
} else {
519+
this.videojs.removeClass('cld-mobile-touch-playing');
520+
this._removeMobileBigPlayButtonHandler();
521+
}
522+
}
523+
524+
_setupMobileBigPlayButtonHandler(isPlaying) {
525+
if (!isPlaying) {
526+
return; // Use default play behavior when paused
527+
}
528+
529+
// Remove any existing handler first
530+
this._removeMobileBigPlayButtonHandler();
531+
532+
// Find the big play button
533+
const bigPlayButton = this.videojs.el().querySelector('.vjs-big-play-button');
534+
if (!bigPlayButton) {
535+
return;
536+
}
537+
538+
// Create custom pause handler that completely overrides default behavior
539+
this._mobilePauseHandler = (event) => {
540+
event.preventDefault();
541+
event.stopPropagation();
542+
event.stopImmediatePropagation();
543+
544+
// Pause the video
545+
if (!this.videojs.paused()) {
546+
this.videojs.pause();
547+
548+
// Immediately update the mobile touch state
549+
setTimeout(() => {
550+
this._updateMobileTouchState();
551+
552+
// Force show the button briefly to give visual feedback
553+
if (this.videojs.hasClass('cld-mobile-touch-active')) {
554+
this.videojs.clearTimeout(this._mobileTouchTimeout);
555+
this._mobileTouchTimeout = this.videojs.setTimeout(() => {
556+
this.videojs.removeClass('cld-mobile-touch-active');
557+
}, 2000);
558+
}
559+
}, 50);
560+
}
561+
562+
return false;
563+
};
564+
565+
// Add multiple event listeners to ensure we catch the click
566+
bigPlayButton.addEventListener('click', this._mobilePauseHandler, { capture: true });
567+
bigPlayButton.addEventListener('touchend', this._mobilePauseHandler, { capture: true });
568+
bigPlayButton.addEventListener('mousedown', this._mobilePauseHandler, { capture: true });
569+
570+
// Also disable the VideoJS component's click handling temporarily
571+
const bigPlayButtonComponent = this.videojs.getChild('bigPlayButton');
572+
if (bigPlayButtonComponent) {
573+
this._originalHandleClick = bigPlayButtonComponent.handleClick;
574+
bigPlayButtonComponent.handleClick = () => {
575+
// Override with our pause behavior
576+
this._mobilePauseHandler({
577+
preventDefault: () => {},
578+
stopPropagation: () => {},
579+
stopImmediatePropagation: () => {}
580+
});
581+
};
582+
}
583+
584+
// Store reference to the button for cleanup
585+
this._mobilePauseButton = bigPlayButton;
586+
}
587+
588+
_removeMobileBigPlayButtonHandler() {
589+
if (this._mobilePauseHandler && this._mobilePauseButton) {
590+
// Remove all event listeners
591+
this._mobilePauseButton.removeEventListener('click', this._mobilePauseHandler, { capture: true });
592+
this._mobilePauseButton.removeEventListener('touchend', this._mobilePauseHandler, { capture: true });
593+
this._mobilePauseButton.removeEventListener('mousedown', this._mobilePauseHandler, { capture: true });
594+
595+
// Restore original VideoJS component behavior
596+
const bigPlayButtonComponent = this.videojs.getChild('bigPlayButton');
597+
if (bigPlayButtonComponent && this._originalHandleClick) {
598+
bigPlayButtonComponent.handleClick = this._originalHandleClick;
599+
this._originalHandleClick = null;
600+
}
601+
602+
this._mobilePauseHandler = null;
603+
this._mobilePauseButton = null;
604+
}
605+
}
606+
409607
_initCloudinary() {
410608
const cloudinaryConfig = this.playerOptions.cloudinary;
411609
cloudinaryConfig.chainTarget = this;

0 commit comments

Comments
 (0)