"use strict" /** custom html element This acts as a preview for youtube's embed player. It's gross and I hate it, but it's necessary, because their site is WAY too bloated for us to load it automatically. Usage: `href` can be any valid youtube video or youtube shorts url Known query parameters are: • t=, start= - start time (in seconds) • end= - end time (in seconds) • loop - enable looping When generating html, you may want to do something like this: {url} That way, it's still accessible if the custom element isn't installed. **/ class Markup_YoutubeElement extends HTMLElement { constructor() { super() this.attachShadow({mode: 'open'}) let e = this.constructor.template() for (let elem of e.querySelectorAll("[id]")) this["$"+elem.id] = elem this.$link.onclick = ev=>{ ev.preventDefault() this.show_youtube(true) } this.$close.onclick = ev=>{ this.show_youtube(false) } this.shadowRoot.append(e) } show_youtube(state) { this.$close.hidden = !state this.toggleAttribute('data-big', state) if (!this.$iframe == !state) return if (state) { this.$iframe = document.createElement('iframe') let src = `https://www.youtube-nocookie.com/embed/${this._id}?autoplay=1&rel=0` if (this._query) src += `&${this._query}` this.$iframe.src = src this.$iframe.allowFullscreen = true this.$link.replaceWith(this.$iframe) } else { this.$iframe.replaceWith(this.$link) this.$iframe.src = "about:blank" this.$iframe = null } } connectedCallback() { this.update_href(this.dataset.href) } disconnectedCallback() { this._id = null } update_href(url) { if (!url) return // todo: allow setting back to unloaded state? url = url.replace("/shorts/", "/watch?v=") // 🤮 url = url.replace("://music.", "://www.") // i hope this works if (this._href == url) return this._href = url this.$title.textContent = url this.$author.textContent = "" this.$link.href = url let [, id, query] = /^https?:[/][/](?:www[.])?(?:youtube.com[/]watch[?]v=|youtu[.]be[/])([\w-]{11,})([&?].*)?$/.exec(url) if (query) { function parse_time(str) { let r = /^(?:([0-9.]+)h)?(?:([0-9.]+)m)?([0-9.]+)s?$/.exec(str) if (r) { let [_, h=0, m=0, s=0] = r return +h*3600 + +m*60 + +s } return str } function render_time(n) { if (!n) return "x" let s = n % 60 | 0 n /= 60 let m = n % 60 | 0 n /= 60 let h = n % 60 | 0 if (h) return h+":"+m+":"+s return m+":"+s } let time = /[&?](?:t|start)=([^&?]+)/.exec(query) let end = /[&?]end=([^&?]+)/.exec(query) let loop = /[&?]loop(?:=|&|$)/.exec(query) query = "" if (time) { time = parse_time(time[1]) query += "&start="+time } if (end) { end = parse_time(end[1]) query += "&end="+end } //if (loop) query += "&loop=1&playlist="+id // this is broken now.. let info = "" if (time && end) info = "at "+render_time(time)+" – "+render_time(end) else if (time) info = "at "+render_time(time) this.$how.textContent = info } this._query = query // display video info if (this._id == id) return this.$link.style.backgroundImage = `url(https://i.ytimg.com/vi/${id}/mqdefault.jpg)` this._id = id // only do one at a time let f = Markup_YoutubeElement.requests[id] if (!f) { // todo: cancel these when node is disconnected? Markup_YoutubeElement.requests[id] = f = fetch(`https://www.youtube.com/oembed?url=https%3A//youtube.com/watch%3Fv%3D${id}&format=json`).then(x=>x.json()).catch(x=>null) } f.then(data=>{ if (this._id != id) return // if the video changed if (!data) data = {title: url, author_name: "(metadata request failed)"} this.$title.textContent = data.title this.$author.textContent = data.author_name }) } attributeChangedCallback(name, old, value) { if (name=='data-href') this.update_href(value) } } // intern these? Markup_YoutubeElement.requests = {} Markup_YoutubeElement.observedAttributes = ['data-href'] { let template = ([html])=>{ let temp = document.createElement('template') temp.innerHTML = html.replace(/\s*?\n\s*/g, "") return document.importNode.bind(document, temp.content, true) } Markup_YoutubeElement.template = template` ❌ close ` } customElements.define('youtube-embed', Markup_YoutubeElement)