import { formatTime, isMobile } from './utils' interface Opts { progressBarTimeTriggerTap?: number progressBarTapDelay?: number progressTimeStepSeconds?: number } const defaultOpts = { progressBarTimeTriggerTap: 0, progressBarTapDelay: 0, progressTimeStepSeconds: 5, } class ProgressBar { private lastTimeUpdate = 0 private touchStarted = false private mousePressed = false private container: HTMLElement private mainBar: HTMLElement private cursor: HTMLElement private timeText: HTMLElement private progressBar: HTMLElement private videoEl: HTMLVideoElement private timeoutPressed?: number private onWindowMouseMoveListener: (e: MouseEvent) => void private onWindowMouseUpListener: (e: MouseEvent) => void private opts: Required constructor(videoEl: HTMLVideoElement, opts: Opts) { this.videoEl = videoEl this.opts = { ...defaultOpts, ...opts } this.onWindowMouseMoveListener = this.windowMouseMoveListener.bind(this) this.onWindowMouseUpListener = this.windowMouseUpListener.bind(this) const container = document.createElement('div') container.classList.add('cp-clip__video-progress-bar') const mainBar = document.createElement('div') mainBar.classList.add('cp-clip__progress-main-bar') const progressBar = document.createElement('div') progressBar.classList.add('cp-clip__progress-bar') const cursor = document.createElement('div') cursor.classList.add('cp-clip__progress-cursor') const timeText = document.createElement('span') timeText.classList.add('cp-clip__progress-time') mainBar.appendChild(progressBar) mainBar.appendChild(cursor) mainBar.appendChild(timeText) container.appendChild(mainBar) this.videoEl.addEventListener('timeupdate', () => { this.update() }) this.videoEl.addEventListener('pause', () => { mainBar.style.transition = 'none' }) this.videoEl.addEventListener('ended', () => { mainBar.classList.add('cp-clip__progress-ended') }) this.videoEl.addEventListener('playing', () => { mainBar.classList.remove('cp-clip__progress-ended') }) container.addEventListener('touchstart', () => { this.touchStarted = true mainBar.classList.add('cp-clip__progress-enlarged') if (this.timeoutPressed) { window.clearTimeout(this.timeoutPressed) } }) const touchEnd = (e: TouchEvent) => { this.touchStarted = false this.timeoutPressed = window.setTimeout(() => { mainBar.classList.remove('cp-clip__progress-enlarged') }, this.opts.progressBarTapDelay) if (e.changedTouches[0]) { this.scrub(e.changedTouches[0].clientX) } e.preventDefault() } container.addEventListener('touchend', touchEnd) container.addEventListener('touchcancel', touchEnd) container.addEventListener('touchmove', (e) => { if (e.changedTouches[0]) { const x = e.changedTouches[0].clientX this.renderProgress(this.getProgressFromPosition(x), false) } }) //When mousedown for progressBarTimeTriggerTap time we dispatch the tap event container.addEventListener('mousedown', () => { if (!isMobile()) { this.mousePressed = true window.addEventListener('mouseup', this.onWindowMouseUpListener) window.addEventListener('mousemove', this.onWindowMouseMoveListener) } }) container.addEventListener('mouseover', () => { if (!isMobile()) { mainBar.classList.add('cp-clip__progress-enlarged') } }) container.addEventListener('mouseleave', () => { if (!this.mousePressed && !isMobile()) { mainBar.classList.remove('cp-clip__progress-enlarged') } }) this.mainBar = mainBar this.cursor = cursor this.progressBar = progressBar this.timeText = timeText this.container = container } /** * Update rendering * @param progress * @param animation */ update(animation = true) { const progress = this.getProgressFromTime() if (!this.touchStarted && !this.mousePressed) { this.renderProgress(progress, animation) } } scrub(x: number) { const scrubberRect = this.mainBar.getBoundingClientRect() const offsetX = x - scrubberRect.left const scrubberWidth = scrubberRect.width // Calculate the new time based on the scrubber's position const newTime = (offsetX / scrubberWidth) * this.videoEl.duration // Set the video's current time this.videoEl.currentTime = newTime this.renderProgress(this.getProgressFromTime(), false) } /** * Render progress bar * @param progress * @param animation */ renderProgress(progress: number | null = null, animation = true) { if (progress === null) { progress = this.getProgressFromTime() } const now = Date.now() const durationAnimation = !this.lastTimeUpdate ? 250 : now - this.lastTimeUpdate this.lastTimeUpdate = now this.progressBar.style.transition = animation ? `width ${durationAnimation}ms linear` : 'none' this.progressBar.style.width = `${progress}%` this.cursor.style.left = `${progress}%` this.timeText.style.left = `clamp(5%, ${progress}%, 95%)` const timeFromProgress = !animation this.updateTimeText(progress, timeFromProgress) } step(backward = false) { const newTime = backward ? this.videoEl.currentTime - this.opts.progressTimeStepSeconds : this.videoEl.currentTime + this.opts.progressTimeStepSeconds this.videoEl.currentTime = newTime this.update(false) this.mainBar.classList.add('cp-clip__progress-enlarged') if (this.timeoutPressed) { window.clearTimeout(this.timeoutPressed) } this.timeoutPressed = window.setTimeout(() => { this.mainBar.classList.remove('cp-clip__progress-enlarged') }, this.opts.progressBarTapDelay) } stepForward() { this.step() } stepBackward() { this.step(true) } getProgressFromPosition(x: number) { const start = this.mainBar.getBoundingClientRect().left const end = this.mainBar.getBoundingClientRect().right return x <= start ? 0 : x >= end ? 100 : ((x - start) * 100) / (end - start) } getProgressFromTime(time: number | null = null) { if (time === null) { return (this.videoEl.currentTime * 100) / this.videoEl.duration } time = time > this.videoEl.duration ? this.videoEl.duration : time < 0 ? 0 : time return (time * 100) / this.videoEl.duration } getTimeFromProgress(progress = 0) { return (this.videoEl.duration * progress) / 100 } getContainer() { return this.container } /** * * @param progress , progress of the video from 0 to 100 * @param timeFromProgress , flag to get the time from the progress or from video currentTime */ updateTimeText(progress: number | null = null, timeFromProgress = false) { if (progress === null) { progress = this.getProgressFromTime() as number } const timeFormatted = timeFromProgress ? formatTime(this.getTimeFromProgress(progress)) : formatTime(this.videoEl.currentTime) if (timeFormatted !== this.timeText.textContent) { this.timeText.textContent = timeFormatted } } windowMouseMoveListener(e: MouseEvent) { if (this.mousePressed) { this.renderProgress(this.getProgressFromPosition(e.clientX), false) } } windowMouseUpListener(e: MouseEvent) { if (this.mousePressed) { this.scrub(e.clientX) this.mousePressed = false if (!e.target || !this?.container?.contains(e.target as Node)) { this.mainBar.classList.remove('cp-clip__progress-enlarged') } window.removeEventListener('mouseup', this.onWindowMouseUpListener) window.removeEventListener('mousemove', this.onWindowMouseMoveListener) } } destroy() { if (this.onWindowMouseMoveListener) window.removeEventListener('mousemove', this.onWindowMouseMoveListener) if (this.onWindowMouseUpListener) window.removeEventListener('mouseup', this.onWindowMouseUpListener) } } export default ProgressBar