Skip to content
Open
121 changes: 118 additions & 3 deletions js/mediaView.js
Original file line number Diff line number Diff line change
Expand Up @@ -536,10 +536,125 @@ class MediaView extends ComponentView {
*/
preventForwardScrubbing() {
if (!this.model.get('_preventForwardScrubbing') || this.model.get('_isComplete')) return;
$(this.mediaElement).on({
seeking: this.onMediaElementSeeking,
timeupdate: this.onMediaElementTimeUpdate

const player = this.mediaElement;
const $slider = this.$('.mejs__time-slider');
if (!$slider.length) return;

let maxViewed = this.model.get('_maxViewed') || 0;
let suppress = false;

// Create and setup the scrub blocker
const scrubBlocker = this.createScrubBlocker($slider[0]);

this.setupScrubBlockerEvents(player, scrubBlocker, () => maxViewed, (newMax) => {
maxViewed = newMax;
this.model.set('_maxViewed', maxViewed);
}, () => suppress, (newSuppress) => {
suppress = newSuppress;
});

// Initialize the blocker size
this.updateScrubBlocker(scrubBlocker, player, maxViewed);
}

createScrubBlocker(sliderElement) {
const scrubBlocker = document.createElement('div');
scrubBlocker.className = 'mejs__time-slider-blocker';

const flashBlockedOverlay = () => {
scrubBlocker.classList.add('mejs__time-slider-blocker-error');
setTimeout(() => {
scrubBlocker.classList.remove('mejs__time-slider-blocker-error');
}, 150);
};

scrubBlocker.addEventListener('pointerdown', (e) => {
e.preventDefault();
e.stopImmediatePropagation();
flashBlockedOverlay();
});

sliderElement.addEventListener('click', (e) => {
const rect = sliderElement.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const sliderWidth = rect.width;
const clickPercent = clickX / sliderWidth;

const player = this.mediaElement;
const duration = player.duration;
const clickTime = clickPercent * duration;
const maxViewed = this.model.get('_maxViewed') || 0;

// If clicking ahead of maxViewed, navigate to maxViewed and flash
if (clickTime > maxViewed + 0.25) {
e.preventDefault();
e.stopImmediatePropagation();
player.currentTime = maxViewed;
flashBlockedOverlay();
}
});

sliderElement.style.position = 'relative';
sliderElement.appendChild(scrubBlocker);
sliderElement.setAttribute('aria-disabled', 'true');

return scrubBlocker;
}

setupScrubBlockerEvents(player, scrubBlocker, getMaxViewed, setMaxViewed, getSuppress, setSuppress) {
// Update progress and blocker size
player.addEventListener('timeupdate', () => {
if (!getSuppress()) {
const newMaxViewed = Math.max(getMaxViewed(), player.currentTime);
setMaxViewed(newMaxViewed);
this.updateScrubBlocker(scrubBlocker, player, newMaxViewed);
}
});

// Prevent forward seeking and navigate to maxViewed
player.addEventListener('seeking', () => {
if (player.currentTime > getMaxViewed() + 0.25) {
setSuppress(true);
player.currentTime = getMaxViewed();
setSuppress(false);

scrubBlocker.classList.add('mejs__time-slider-blocker-error');
setTimeout(() => {
scrubBlocker.classList.remove('mejs__time-slider-blocker-error');
}, 150);

this._showBlockedScrubMessage?.();
}
});

// Prevent keyboard forward navigation
player.addEventListener('keydown', (e) => {
const forwardKeys = ['ArrowRight', 'End', 'PageDown'];
if (forwardKeys.includes(e.code) && player.currentTime >= getMaxViewed()) {
e.preventDefault();

player.currentTime = getMaxViewed();

scrubBlocker.classList.add('mejs__time-slider-blocker-error');
setTimeout(() => {
scrubBlocker.classList.remove('mejs__time-slider-blocker-error');
}, 150);
}
});

player.addEventListener('ended', () => {
this.model.set('_isComplete', true);
scrubBlocker.remove();
});
}

updateScrubBlocker(scrubBlocker, player, maxViewed) {
const duration = player.duration;
if (!duration || duration === Infinity) return;

const percentViewed = 1 - maxViewed / duration;
scrubBlocker.style.width = `${percentViewed * 100}%`;
}

/**
Expand Down
17 changes: 17 additions & 0 deletions less/mediaelementplayer.less
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,23 @@ Reference: http://blog.rrwd.nl/2015/04/04/the-screen-reader-text-class-why-and-h
width: 0;
}

.mejs__time-slider-blocker {
position: absolute;
top: 0;
right: 0;
height: 100%;
width: 0;
background-color: transparent;
pointer-events: auto;
z-index: 9999;
cursor: not-allowed;
transition: background-color 0.15s ease;

&.mejs__time-slider-blocker-error {
background-color: fade(@validation-error, 30%);
}
}

.mejs__long-video .mejs__time-float {
margin-left: -1.4375rem;
width: 4rem;
Expand Down