live_beats/assets/js/app.js

334 lines
10 KiB
JavaScript
Raw Normal View History

2021-09-02 18:00:57 +00:00
import "phoenix_html"
import {Socket} from "phoenix"
2023-03-06 15:57:42 +00:00
import {LiveSocket} from "phoenix_live_view"
2021-09-02 18:00:57 +00:00
import topbar from "../vendor/topbar"
2023-01-27 00:39:59 +00:00
import Sortable from "../vendor/sortable"
2021-09-02 18:00:57 +00:00
2021-11-05 19:57:33 +00:00
let nowSeconds = () => Math.round(Date.now() / 1000)
2022-01-27 18:03:42 +00:00
let rand = (min, max) => Math.floor(Math.random() * (max - min) + min)
let isVisible = (el) => !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0)
2021-10-27 20:02:56 +00:00
2021-11-08 19:32:40 +00:00
let execJS = (selector, attr) => {
document.querySelectorAll(selector).forEach(el => liveSocket.execJS(el, el.getAttribute(attr)))
}
2021-10-27 20:02:56 +00:00
let Hooks = {}
2023-01-26 19:05:24 +00:00
Hooks.Sortable = {
mounted(){
let sorter = new Sortable(this.el, {
animation: 150,
onEnd: e => {
this.pushEvent(this.el.dataset["drop"], {id: e.item.id, old: e.oldIndex, new: e.newIndex})
}
})
}
}
2021-11-19 14:55:26 +00:00
Hooks.Flash = {
mounted(){
let hide = () => liveSocket.execJS(this.el, this.el.getAttribute("phx-click"))
this.timer = setTimeout(() => hide(), 8000)
this.el.addEventListener("phx:hide-start", () => clearTimeout(this.timer))
this.el.addEventListener("mouseover", () => {
clearTimeout(this.timer)
this.timer = setTimeout(() => hide(), 8000)
})
},
destroyed(){ clearTimeout(this.timer) }
}
2021-11-18 14:55:09 +00:00
Hooks.Menu = {
getAttr(name){
let val = this.el.getAttribute(name)
if(val === null){ throw(new Error(`no ${name} attribute configured for menu`)) }
return val
},
reset(){
this.enabled = false
2021-11-18 14:55:09 +00:00
this.activeClass = this.getAttr("data-active-class")
this.deactivate(this.menuItems())
this.activeItem = null
window.removeEventListener("keydown", this.handleKeyDown)
2021-11-18 14:55:09 +00:00
},
destroyed(){ this.reset() },
2021-11-18 14:55:09 +00:00
mounted(){
this.menuItemsContainer = document.querySelector(`[aria-labelledby="${this.el.id}"]`)
this.reset()
2021-11-18 21:01:29 +00:00
this.handleKeyDown = (e) => this.onKeyDown(e)
this.el.addEventListener("keydown", e => {
if((e.key === "Enter" || e.key === " ") && e.currentTarget.isSameNode(this.el)){
this.enabled = true
}
})
2021-11-18 14:55:09 +00:00
this.el.addEventListener("click", e => {
2021-11-18 21:01:29 +00:00
if(!e.currentTarget.isSameNode(this.el)){ return }
window.addEventListener("keydown", this.handleKeyDown)
// disable if button clicked and click was not a keyboard event
if(this.enabled){
window.requestAnimationFrame(() => this.activate(0))
2021-11-18 14:55:09 +00:00
}
})
2021-11-22 16:21:01 +00:00
this.menuItemsContainer.addEventListener("phx:hide-start", () => this.reset())
2021-11-18 14:55:09 +00:00
},
activate(index, fallbackIndex){
let menuItems = this.menuItems()
this.activeItem = menuItems[index] || menuItems[fallbackIndex]
this.activeItem.classList.add(this.activeClass)
this.activeItem.focus()
2021-11-18 14:55:09 +00:00
},
deactivate(items){ items.forEach(item => item.classList.remove(this.activeClass)) },
menuItems(){ return Array.from(this.menuItemsContainer.querySelectorAll("[role=menuitem]")) },
onKeyDown(e){
if(e.key === "Escape"){
document.body.click()
2021-11-18 21:17:47 +00:00
this.el.focus()
this.reset()
} else if(e.key === "Enter" && !this.activeItem){
this.activate(0)
} else if(e.key === "Enter"){
this.activeItem.click()
}
if(e.key === "ArrowDown"){
e.preventDefault()
let menuItems = this.menuItems()
this.deactivate(menuItems)
2021-11-18 21:01:29 +00:00
this.activate(menuItems.indexOf(this.activeItem) + 1, 0)
} else if(e.key === "ArrowUp"){
e.preventDefault()
let menuItems = this.menuItems()
this.deactivate(menuItems)
2021-11-18 21:01:29 +00:00
this.activate(menuItems.indexOf(this.activeItem) - 1, menuItems.length - 1)
2021-11-22 19:28:57 +00:00
} else if (e.key === "Tab"){
e.preventDefault()
}
}
2021-11-18 14:55:09 +00:00
}
2021-11-05 00:49:19 +00:00
Hooks.AudioPlayer = {
mounted(){
this.playbackBeganAt = null
this.player = this.el.querySelector("audio")
this.currentTime = this.el.querySelector("#player-time")
2022-01-27 19:41:26 +00:00
this.duration = this.el.querySelector("#player-duration")
2021-11-05 00:49:19 +00:00
this.progress = this.el.querySelector("#player-progress")
let enableAudio = () => {
2021-11-08 18:46:23 +00:00
if(this.player.src){
document.removeEventListener("click", enableAudio)
2021-11-16 16:58:22 +00:00
if(this.player.readyState === 0){
this.player.play().catch(error => null)
this.player.pause()
}
2021-11-08 18:46:23 +00:00
}
2021-11-05 00:49:19 +00:00
}
document.addEventListener("click", enableAudio)
2021-11-05 19:57:33 +00:00
this.el.addEventListener("js:listen_now", () => this.play({sync: true}))
2021-11-05 00:49:19 +00:00
this.el.addEventListener("js:play_pause", () => {
2021-11-05 19:57:33 +00:00
if(this.player.paused){
this.play()
}
2021-11-05 00:49:19 +00:00
})
this.handleEvent("play", ({url, token, elapsed, artist, title}) => {
2021-11-05 19:57:33 +00:00
this.playbackBeganAt = nowSeconds() - elapsed
2021-11-06 03:02:31 +00:00
let currentSrc = this.player.src.split("?")[0]
if(currentSrc === url && this.player.paused){
2021-11-05 19:57:33 +00:00
this.play({sync: true})
2021-11-06 03:02:31 +00:00
} else if(currentSrc !== url) {
2022-01-27 19:41:26 +00:00
this.player.src = `${url}?token=${token}`
2021-11-05 19:57:33 +00:00
this.play({sync: true})
}
if("mediaSession" in navigator){
navigator.mediaSession.metadata = new MediaMetadata({artist, title})
}
2021-11-05 00:49:19 +00:00
})
2021-11-12 03:42:10 +00:00
this.handleEvent("pause", () => this.pause())
this.handleEvent("stop", () => this.stop())
2021-11-05 00:49:19 +00:00
},
2022-01-27 19:41:26 +00:00
clearNextTimer(){
clearTimeout(this.nextTimer)
this.nextTimer = null
},
2021-11-05 19:57:33 +00:00
play(opts = {}){
let {sync} = opts
2022-01-27 19:41:26 +00:00
this.clearNextTimer()
2021-11-05 00:49:19 +00:00
this.player.play().then(() => {
2022-01-27 19:41:26 +00:00
if(sync){ this.player.currentTime = nowSeconds() - this.playbackBeganAt }
2021-11-05 00:49:19 +00:00
this.progressTimer = setInterval(() => this.updateProgress(), 100)
}, error => {
if(error.name === "NotAllowedError"){
execJS("#enable-audio", "data-js-show")
}
})
2021-11-05 00:49:19 +00:00
},
pause(){
clearInterval(this.progressTimer)
2021-11-05 19:57:33 +00:00
this.player.pause()
2021-11-05 00:49:19 +00:00
},
2021-11-12 03:42:10 +00:00
stop(){
clearInterval(this.progressTimer)
this.player.pause()
this.player.currentTime = 0
this.updateProgress()
2022-01-27 19:41:26 +00:00
this.duration.innerText = ""
2021-11-12 03:42:10 +00:00
this.currentTime.innerText = ""
},
2021-11-05 00:49:19 +00:00
updateProgress(){
2022-01-27 19:41:26 +00:00
if(isNaN(this.player.duration)){ return false }
if(!this.nextTimer && this.player.currentTime >= this.player.duration){
2021-11-10 19:29:53 +00:00
clearInterval(this.progressTimer)
2022-01-27 19:41:26 +00:00
this.nextTimer = setTimeout(() => this.pushEvent("next_song_auto"), rand(0, 1500))
2021-11-10 19:29:53 +00:00
return
}
2022-01-27 19:41:26 +00:00
this.progress.style.width = `${(this.player.currentTime / (this.player.duration) * 100)}%`
this.duration.innerText = this.formatTime(this.player.duration)
2021-11-05 00:49:19 +00:00
this.currentTime.innerText = this.formatTime(this.player.currentTime)
},
formatTime(seconds){ return new Date(1000 * seconds).toISOString().substr(14, 5) }
}
2022-01-28 01:42:36 +00:00
Hooks.Ping = {
mounted(){
this.handleEvent("pong", () => {
2022-01-29 01:40:48 +00:00
let rtt = Date.now() - this.nowMs
this.el.innerText = `ping: ${rtt}ms`
2022-11-17 15:01:20 +00:00
// this.timer = setTimeout(() => this.ping(rtt), 1000)
2022-01-28 01:42:36 +00:00
})
2022-01-29 01:40:48 +00:00
this.ping(null)
2022-01-28 01:42:36 +00:00
},
2022-02-10 18:40:58 +00:00
reconnected(){
clearTimeout(this.timer)
this.ping(null)
},
2022-01-28 01:42:36 +00:00
destroyed(){ clearTimeout(this.timer) },
2022-01-29 01:40:48 +00:00
ping(rtt){
2022-01-28 01:42:36 +00:00
this.nowMs = Date.now()
2022-01-29 01:40:48 +00:00
this.pushEvent("ping", {rtt: rtt})
2022-01-28 01:42:36 +00:00
}
}
// Accessible focus handling
let Focus = {
focusMain(){
let target = document.querySelector("main h1") || document.querySelector("main")
if(target){
let origTabIndex = target.tabIndex
target.tabIndex = -1
target.focus()
target.tabIndex = origTabIndex
}
},
// Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
isFocusable(el){
if(el.tabIndex > 0 || (el.tabIndex === 0 && el.getAttribute("tabIndex") !== null)){ return true }
if(el.disabled){ return false }
switch(el.nodeName) {
case "A":
return !!el.href && el.rel !== "ignore"
case "INPUT":
return el.type != "hidden" && el.type !== "file"
case "BUTTON":
case "SELECT":
case "TEXTAREA":
return true
default:
return false
}
},
// Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
attemptFocus(el){
if(!el){ return }
if(!this.isFocusable(el)){ return false }
try {
el.focus()
} catch(e){}
return document.activeElement === el
},
// Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
focusFirstDescendant(el){
for(let i = 0; i < el.childNodes.length; i++){
let child = el.childNodes[i]
if(this.attemptFocus(child) || this.focusFirstDescendant(child)){
return true
}
}
return false
},
// Subject to the W3C Software License at https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
focusLastDescendant(element){
for(let i = element.childNodes.length - 1; i >= 0; i--){
let child = element.childNodes[i]
if(this.attemptFocus(child) || this.focusLastDescendant(child)){
return true
}
}
return false
},
}
2021-09-02 18:00:57 +00:00
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
2021-11-19 14:55:26 +00:00
hooks: Hooks,
params: {_csrf_token: csrfToken},
dom: {
onNodeAdded(node){
if(node instanceof HTMLElement && node.autofocus){
node.focus()
}
}
}
2021-09-02 18:00:57 +00:00
})
2022-05-23 20:13:30 +00:00
let routeUpdated = () => {
Focus.focusMain()
}
2021-09-02 18:00:57 +00:00
// Show progress bar on live navigation and form submits
2022-01-14 16:05:41 +00:00
topbar.config({barColors: {0: "rgba(147, 51, 234, 1)"}, shadowColor: "rgba(0, 0, 0, .3)"})
2022-08-03 13:40:11 +00:00
window.addEventListener("phx:page-loading-start", info => topbar.delayedShow(200))
2021-09-02 18:00:57 +00:00
window.addEventListener("phx:page-loading-stop", info => topbar.hide())
// Accessible routing
2022-05-23 20:13:30 +00:00
window.addEventListener("phx:page-loading-stop", routeUpdated)
2021-11-05 00:49:19 +00:00
window.addEventListener("js:exec", e => e.target[e.detail.call](...e.detail.args))
window.addEventListener("js:focus", e => {
let parent = document.querySelector(e.detail.parent)
if(parent && isVisible(parent)){ e.target.focus() }
})
window.addEventListener("js:focus-closest", e => {
let el = e.target
let sibling = el.nextElementSibling
while(sibling){
if(isVisible(sibling) && Focus.attemptFocus(sibling)){ return }
sibling = sibling.nextElementSibling
}
sibling = el.previousElementSibling
while(sibling){
if(isVisible(sibling) && Focus.attemptFocus(sibling)){ return }
sibling = sibling.previousElementSibling
}
Focus.attemptFocus(el.parent) || Focus.focusMain()
})
window.addEventListener("phx:remove-el", e => document.getElementById(e.detail.id).remove())
2021-11-05 00:49:19 +00:00
2021-09-02 18:00:57 +00:00
// connect if there are any LiveViews on the page
2021-11-22 14:24:36 +00:00
liveSocket.getSocket().onOpen(() => execJS("#connection-status", "js-hide"))
liveSocket.getSocket().onError(() => execJS("#connection-status", "js-show"))
2021-09-02 18:00:57 +00:00
liveSocket.connect()
// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
2023-01-26 19:05:24 +00:00
window.liveSocket = liveSocket