Skip to content

Commit

Permalink
feat: vertical volume control component
Browse files Browse the repository at this point in the history
  • Loading branch information
alexarassat committed May 24, 2024
1 parent 8847f98 commit 459c877
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 1 deletion.
2 changes: 1 addition & 1 deletion flowplayer.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {type FlowplayerUMD} from "@flowplayer/player"
import { FlowplayerUMD} from "@flowplayer/player"
declare global {
var flowplayer: FlowplayerUMD;
}
84 changes: 84 additions & 0 deletions packages/flowplayer-vertical-volume-control/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { install } from "../../shared/install"
import { type Player } from "@flowplayer/player"
import {CLICK, VOLUME_CHANGE} from "@flowplayer/player/core/events"
import support from "../utils"
import { SliderStates, makeSlider } from "./slider"


export default class FlowplayerVerticalVolumeControl extends HTMLElement {

constructor(player: Player) {
super()
this.classList.add("fp-volume-control-vertical")

const volume_icon = player.createComponents(flowplayer.defaultElements.VOLUME_ICON)[0]
volume_icon.addEventListener(CLICK, _ => player.toggleMute())
this.append(volume_icon)

if (support().ios || support().android) return

const volumeBar = this.createVolumeBar(player)
makeSlider(volumeBar, {onseek: this.onVolumeBarSeek.bind(this, player)})
this.append(volumeBar)

player.on(VOLUME_CHANGE, () => {
const muted = (player.volume == 0) || player.muted
if (muted) this.adjustVolumeSlider(player, 0, volumeBar)

const ui_opt = player.opt("ui")
if (!this.classList.contains(SliderStates.GRABBING) && !(muted && typeof ui_opt === "number" && (4 & ui_opt) > 0)) {
this.adjustVolumeSlider(player, player.volume, volumeBar)
}
})

volume_icon.addEventListener("pointerenter", _ => volumeBar.style.opacity = "1")
this.addEventListener("pointerleave", _ => volumeBar.style.opacity = "0")
}

createVolumeBar(player: Player) {
const volumeBar = document.createElement("div")
volumeBar.classList.add("fp-volume-vertical")
volumeBar.setAttribute("tabindex", "0")
volumeBar.setAttribute("role", "slider")
volumeBar.setAttribute("aria-valuemin", "0")
volumeBar.setAttribute("aria-valuemax", "1")
volumeBar.setAttribute("aria-label", player.i18n("core.volume", "volume"))

const container = document.createElement("div")
container.setAttribute("aria-hidden", "true")
container.classList.add("fp-volume-container")

const volume = document.createElement("div")
volume.classList.add("fp-volume-progress", "fp-color", "use-drag-handle")

const dragger = document.createElement("div")
dragger.classList.add("fp-dragger", "fp-color")


volume.append(dragger)
volumeBar.append(container)
container.append(volume)

return volumeBar
}

onVolumeBarSeek(player: Player, volumeBar: HTMLElement, amount: number) {
player._storage.setItem("volume", (player.volume = amount / 100).toString())
this.adjustVolumeSlider(player, player.volume, volumeBar)

if (amount < 0) return
player.muted = false
player._storage.removeItem("mute")
}

adjustVolumeSlider(player: Player, amount: number, volume_bar: HTMLElement) {
volume_bar.setAttribute("aria-valuenow", amount.toString())
volume_bar.setAttribute("aria-valuetext", Math.round(amount * 100) + "%")

const progress = this.querySelector(".fp-volume-progress") as HTMLDivElement
if(!progress) return
progress.style.height = player.muted ? "0" : Math.round(amount * 100) + "%"
}
}
// handle umd installs
install("flowplayer-volume-control", "flowplayer-vertical-volume-control", FlowplayerVerticalVolumeControl)
10 changes: 10 additions & 0 deletions packages/flowplayer-vertical-volume-control/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@flowplayer/components-flowplayer-vertical-volume-control",
"main": "./index.ts",
"description": "A vertical volume bar",
"flowplayer": {
"componentName": "flowplayer-vertical-volume-control",
"overridenComponent": "flowplayer-volume-control",
"className": "FlowplayerVerticalVolumeControl"
}
}
154 changes: 154 additions & 0 deletions packages/flowplayer-vertical-volume-control/slider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { TOUCH_START, TOUCH_MOVE, TOUCH_END, TOUCH_CANCEL,
MOUSE_UP, MOUSE_DOWN, MOUSE_MOVE } from "@flowplayer/player/core/events"

const PASSIVE = {passive: true}

export enum SliderStates
{ GRABBING = "has-grab"
, TOUCHING = "has-touch"
}

export type SliderOpts =
{ onseek?: (r: HTMLElement, n : number)=> void
; onstart?: (r: HTMLElement, n : number)=> void
; onend?: (r: HTMLElement, n : number)=> void
; onmouse?: (r: HTMLElement, n : number)=> void
; ontouch?: (r: HTMLElement, n : number)=> void
; ontouchend?: (r: HTMLElement, n : number)=> void
}

export function makeSlider (root : HTMLElement, opts : SliderOpts) : HTMLElement {
let offset = 0, max = -1, prev = -1
const id = randomElementId()
const classList = root.classList
root.id = id

classList.remove(SliderStates.GRABBING)
classList.remove(SliderStates.TOUCHING)

const onseek = opts.onseek || noop
, onstart = opts.onstart || noop
, onend = opts.onend || noop
, onmouse = opts.onmouse || noop
, ontouch = opts.ontouch || noop
, ontouchend = opts.ontouchend || noop

// root dimensions can change
function calc () {
const rect = root.getBoundingClientRect()
offset = rect.bottom - (parseFloat(window.getComputedStyle(root).paddingBottom))
max = rect.height - parseFloat(window.getComputedStyle(root).paddingBottom) - parseFloat(window.getComputedStyle(root).paddingTop)
}

function extractValue (e : MouseEvent | TouchEvent) {
calc()
const pos = is_touch_event(e) ? e.changedTouches[0].pageY : e.pageY
let val = offset - pos
if (val > max) val = max
if (val < 0) val = 0
return val / max * 100
}

function move (e : MouseEvent | TouchEvent) {
const val = extractValue(e)
if (val == prev) return
onseek(root, val)
prev = val
}

root.addEventListener(TOUCH_START, function(e) {
if (!shouldFire(root, e)) return
//root.touching = true
classList.add(SliderStates.TOUCHING)
if (!is_visible(root.parentElement)) return
//root.grabbing = true
classList.add(SliderStates.GRABBING)
ontouch(root, extractValue(e))
onstart(root, extractValue(e))
move(e)
}, PASSIVE)

root.addEventListener(TOUCH_MOVE, function(e) {
move(e)
onmouse(root, extractValue(e))
}, PASSIVE)

root.addEventListener(TOUCH_END, function(e) {
// mouse-up event is emitted after touchend. Prevent mouse-up event to seek when touched.
setTimeout(function () {
//root.touching = false
classList.remove(SliderStates.TOUCHING)
}, 500)
if (!shouldFire(root, e)) return
//root.grabbing = false
classList.remove(SliderStates.GRABBING)
ontouchend(root, extractValue(e))
onend(root, extractValue(e))
max = 0
}, PASSIVE)

root.addEventListener(TOUCH_CANCEL, function() {
classList.remove(SliderStates.GRABBING, SliderStates.TOUCHING)
max = 0
}, PASSIVE)

root.addEventListener(MOUSE_DOWN, function(e) {
if (classList.contains(SliderStates.TOUCHING)) return
document.addEventListener(MOUSE_MOVE, move)
//root.grabbing = true
classList.add(SliderStates.GRABBING)
onstart(root, extractValue(e))
e.preventDefault()
move(e)
})

root.addEventListener(MOUSE_MOVE, function(e) {
if (classList.contains(SliderStates.TOUCHING)) return
onmouse(root, extractValue(e))
})


// remove listener
document.addEventListener(MOUSE_UP, function (e) {
if (classList.contains(SliderStates.TOUCHING)) return
document.removeEventListener(MOUSE_MOVE, move)
if(!classList.contains(SliderStates.GRABBING)) return
// root.grabbing = false
classList.remove(SliderStates.GRABBING)
onend(root, extractValue(e))
max = 0
})

return root
}

function shouldFire (root : HTMLElement, e : Event) : boolean {
const target = e.target
if (!target) return false
return (
target &&
!(target as any).closest(root.id) ||
!is_visible(root.parentElement) ||
!root.classList.contains(SliderStates.TOUCHING))
}


function is_visible (elem: HTMLElement | null) {
if (!elem) return false
const style = window.getComputedStyle(elem)
return style.width !== "0" &&
style.height !== "0" &&
style.opacity !== "0" &&
style.display !== "none" &&
style.visibility !== "hidden"
}

function is_touch_event (e : any) : e is TouchEvent {
return (typeof window.TouchEvent === "function" && e instanceof TouchEvent)
}

function noop () {}

function randomElementId () {
return Math.random().toString(36).replace(/[^a-z]+/g, "").substr(0, 5)
}
37 changes: 37 additions & 0 deletions packages/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export default function support () {
const in_browser = typeof document !== "undefined" && typeof window !== "undefined"

const UA = in_browser ? navigator.userAgent : ""
, IS_IPHONE = /iP(hone|od)/i.test(UA) && !/iPad/.test(UA) && !/IEMobile/i.test(UA)
, IS_ANDROID = /Android/.test(UA) && !/Firefox/.test(UA)
, IS_SAFARI = /^((?!chrome|android).)*safari/i.test(UA)
, IS_CHROME = (/chrome|crios/i).test(UA) && !(/opr|opera|chromium|edg|ucbrowser|googlebot/i).test(UA)
, IS_FIREFOX = (/firefox|fxios/i).test(UA) && !(/seamonkey/i).test(UA)
, IS_EDGE = (/edg/i).test(UA)
, IS_OPERA= (/opr|opera/i).test(UA)
, IS_SAMSUNG = /SamsungBrowser/.test(UA)
, IS_SAMSUNG_SMART_TV = IS_SAMSUNG && /SMART-TV/.test(UA)

const self =
{ controls : !IS_IPHONE
, video : function (type : string) {return in_browser && document.createElement("video").canPlayType(type)}
, lang : in_browser && window.navigator.language
, android : IS_ANDROID
, iphone : IS_IPHONE
, safari : IS_SAFARI
, edge : IS_EDGE
, opera : IS_OPERA
, chrome : IS_CHROME
, firefox : IS_FIREFOX
, ios : in_browser && (/iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream)
, samsung : IS_SAMSUNG
, samsung_tv : IS_SAMSUNG && IS_SAMSUNG_SMART_TV
, touch : "ontouchstart" in window
, tizen : "tizen" in window
, webOS : "webos" in window
}
return self
}



0 comments on commit 459c877

Please sign in to comment.