this repo has no description
at cactus 217 lines 5.5 kB view raw
1"use strict" 2/** 3 <youtube-embed> custom html element 4 This acts as a preview for youtube's <iframe> embed player. 5 It's gross and I hate it, but it's necessary, 6 because their site is WAY too bloated for us to load it automatically. 7 8 Usage: 9 <youtube-embed href="https://youtu.be/URe5ihr2ow9"></youtube-embed> 10 `href` can be any valid youtube video or youtube shorts url 11 Known query parameters are: 12 • t=, start= - start time (in seconds) 13 • end= - end time (in seconds) 14 • loop - enable looping 15 16 When generating html, you may want to do something like this: 17 <youtube-embed href="{url}"> 18 <a href="{url}">{url}</a> 19 </youtube-embed> 20 That way, it's still accessible if the custom element isn't installed. 21**/ 22class Markup_YoutubeElement extends HTMLElement { 23 constructor() { 24 super() 25 this.attachShadow({mode: 'open'}) 26 let e = this.constructor.template() 27 for (let elem of e.querySelectorAll("[id]")) 28 this["$"+elem.id] = elem 29 this.$link.onclick = ev=>{ 30 ev.preventDefault() 31 this.show_youtube(true) 32 } 33 this.$close.onclick = ev=>{ 34 this.show_youtube(false) 35 } 36 this.shadowRoot.append(e) 37 } 38 show_youtube(state) { 39 this.$close.hidden = !state 40 this.toggleAttribute('data-big', state) 41 if (!this.$iframe == !state) 42 return 43 if (state) { 44 this.$iframe = document.createElement('iframe') 45 let src = `https://www.youtube-nocookie.com/embed/${this._id}?autoplay=1&rel=0` 46 if (this._query) 47 src += `&${this._query}` 48 this.$iframe.src = src 49 this.$iframe.allowFullscreen = true 50 this.$link.replaceWith(this.$iframe) 51 } else { 52 this.$iframe.replaceWith(this.$link) 53 this.$iframe.src = "about:blank" 54 this.$iframe = null 55 } 56 } 57 connectedCallback() { 58 this.update_href(this.dataset.href) 59 } 60 disconnectedCallback() { 61 this._id = null 62 } 63 update_href(url) { 64 if (!url) 65 return // todo: allow setting back to unloaded state? 66 url = url.replace("/shorts/", "/watch?v=") // 🤮 67 url = url.replace("://music.", "://www.") // i hope this works 68 if (this._href == url) 69 return 70 this._href = url 71 this.$title.textContent = url 72 this.$author.textContent = "" 73 this.$link.href = url 74 75 let [, id, query] = /^https?:[/][/](?:www[.])?(?:youtube.com[/]watch[?]v=|youtu[.]be[/])([\w-]{11,})([&?].*)?$/.exec(url) 76 if (query) { 77 function parse_time(str) { 78 let r = /^(?:([0-9.]+)h)?(?:([0-9.]+)m)?([0-9.]+)s?$/.exec(str) 79 if (r) { 80 let [_, h=0, m=0, s=0] = r 81 return +h*3600 + +m*60 + +s 82 } 83 return str 84 } 85 function render_time(n) { 86 if (!n) 87 return "x" 88 let s = n % 60 | 0 89 n /= 60 90 let m = n % 60 | 0 91 n /= 60 92 let h = n % 60 | 0 93 if (h) 94 return h+":"+m+":"+s 95 return m+":"+s 96 } 97 let time = /[&?](?:t|start)=([^&?]+)/.exec(query) 98 let end = /[&?]end=([^&?]+)/.exec(query) 99 let loop = /[&?]loop(?:=|&|$)/.exec(query) 100 query = "" 101 if (time) { 102 time = parse_time(time[1]) 103 query += "&start="+time 104 } 105 if (end) { 106 end = parse_time(end[1]) 107 query += "&end="+end 108 } 109 //if (loop) query += "&loop=1&playlist="+id // this is broken now.. 110 let info = "" 111 if (time && end) 112 info = "at "+render_time(time)+" – "+render_time(end) 113 else if (time) 114 info = "at "+render_time(time) 115 this.$how.textContent = info 116 } 117 this._query = query 118 119 // display video info 120 if (this._id == id) 121 return 122 this.$link.style.backgroundImage = `url(https://i.ytimg.com/vi/${id}/mqdefault.jpg)` 123 this._id = id 124 // only do one at a time 125 let f = Markup_YoutubeElement.requests[id] 126 if (!f) { 127 // todo: cancel these when node is disconnected? 128 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) 129 } 130 f.then(data=>{ 131 if (this._id != id) 132 return // if the video changed 133 if (!data) 134 data = {title: url, author_name: "(metadata request failed)"} 135 this.$title.textContent = data.title 136 this.$author.textContent = data.author_name 137 }) 138 } 139 attributeChangedCallback(name, old, value) { 140 if (name=='data-href') 141 this.update_href(value) 142 } 143} 144// intern these? 145Markup_YoutubeElement.requests = {} 146Markup_YoutubeElement.observedAttributes = ['data-href'] 147{ 148 let template = ([html])=>{ 149 let temp = document.createElement('template') 150 temp.innerHTML = html.replace(/\s*?\n\s*/g, "") 151 return document.importNode.bind(document, temp.content, true) 152 } 153 Markup_YoutubeElement.template = template` 154<a target=_blank id=link> 155 <cite id=caption> 156 <span id=title></span> 157 <div></div> 158 <span id=author></span> 159 </cite> 160</a> 161<div id=how></div> 162<button hidden id=close>❌ close</button> 163<style> 164 :host { 165 display: flex !important; 166 border: 2px solid gray; 167 --height: 135px; 168 flex-direction: column; 169 } 170 :host([data-big]) { 171 --height: 270px; 172 } 173 #close { 174 /*width: 25px;*/ 175 flex-shrink: 0; 176 } 177 iframe { 178 min-width:0; 179 flex-grow:1; 180 border: none; 181 height: var(--height); 182 } 183 #link { 184 height: var(--height); 185 padding: 4px; 186 overflow-y: auto; 187 background: no-repeat 0 / contain; 188 overflow-wrap: break-word; 189 white-space: pre-wrap; 190 flex-grow: 1; 191 box-sizing: border-box; 192 } 193 #caption { 194 background: #0008; 195 color: #FFF; 196 font-family: sans-serif; 197 display: inline; 198 } 199 #caption > span { 200 padding: 0 0.25rem; 201 } 202 #caption > div { 203 height: 5px; 204 } 205 #author { 206 font-style: normal; 207 font-weight: bold; 208 } 209 #how { 210 padding-left: 2px; 211 font-family: monospace; 212 } 213</style> 214` 215} 216 217customElements.define('youtube-embed', Markup_YoutubeElement)