this repo has no description
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)