chore: use remote search
This commit is contained in:
parent
7a2a2aa951
commit
482e13569f
53 changed files with 1184 additions and 3735 deletions
|
@ -1,69 +0,0 @@
|
|||
class ApiText extends HTMLElement {
|
||||
static tagName = 'api-text'
|
||||
|
||||
static register(tagName = this.tagName, registry = globalThis.customElements) {
|
||||
registry.define(tagName, this)
|
||||
}
|
||||
|
||||
static attr = {
|
||||
url: 'api-url',
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this.getAttribute(ApiText.attr.url) || ''
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
if (this.shadowRoot) return
|
||||
|
||||
this.attachShadow({ mode: 'open' }).appendChild(document.createElement('slot'))
|
||||
|
||||
const loading = this.querySelector('.loading')
|
||||
const content = this.querySelector('.content')
|
||||
const cacheKey = this.url || 'api-text-cache'
|
||||
const cache = sessionStorage?.getItem(cacheKey)
|
||||
const noscriptContent = this.querySelector('noscript')?.innerHTML.trim() || ''
|
||||
|
||||
const loadText = (string) => {
|
||||
if (!string) {
|
||||
if (noscriptContent) {
|
||||
content.innerHTML = noscriptContent
|
||||
loading.style.display = 'none'
|
||||
content.style.display = 'block'
|
||||
} else {
|
||||
this.style.display = 'none'
|
||||
}
|
||||
return
|
||||
}
|
||||
loading.style.display = 'none'
|
||||
content.style.display = 'block'
|
||||
content.innerHTML = string
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
loadText(JSON.parse(cache))
|
||||
} else {
|
||||
loading.style.display = 'block'
|
||||
content.style.display = 'none'
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await this.data
|
||||
const value = data.content
|
||||
if (value) {
|
||||
loadText(value)
|
||||
sessionStorage?.setItem(cacheKey, JSON.stringify(value))
|
||||
} else {
|
||||
loadText('')
|
||||
}
|
||||
} catch (error) {
|
||||
loadText('')
|
||||
}
|
||||
}
|
||||
|
||||
get data() {
|
||||
return fetch(this.url).then(response => response.json()).catch(() => ({}))
|
||||
}
|
||||
}
|
||||
|
||||
ApiText.register()
|
|
@ -1,115 +0,0 @@
|
|||
const mastodonPostTemplate = document.createElement("template");
|
||||
|
||||
mastodonPostTemplate.innerHTML = `
|
||||
<figure>
|
||||
<blockquote data-key="content"></blockquote>
|
||||
<figcaption>
|
||||
<cite>
|
||||
<a data-key="url"><span data-key="username"></span>@<span data-key="hostname"></span></a>
|
||||
</cite>
|
||||
<dl>
|
||||
<dt>Reposts</dt><dd data-key="reblogs_count"></dd>
|
||||
<dt>Replies</dt><dd data-key="replies_count"></dd>
|
||||
<dt>Favourites</dt><dd data-key="favourites_count"></dd>
|
||||
</dl>
|
||||
</figcaption>
|
||||
</figure>
|
||||
`;
|
||||
|
||||
mastodonPostTemplate.id = "mastodon-post-template";
|
||||
|
||||
if (!document.getElementById(mastodonPostTemplate.id)) {
|
||||
document.body.appendChild(mastodonPostTemplate);
|
||||
}
|
||||
|
||||
class MastodonPost extends HTMLElement {
|
||||
static register(tagName) {
|
||||
if ("customElements" in window) {
|
||||
customElements.define(tagName || "mastodon-post", MastodonPost);
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this.append(this.template);
|
||||
|
||||
const data = { ...(await this.data), ...this.linkData };
|
||||
|
||||
this.slots.forEach((slot) => {
|
||||
slot.dataset.key.split(",").forEach((keyItem) => {
|
||||
const value = this.getValue(keyItem, data);
|
||||
if (keyItem === "content") {
|
||||
slot.innerHTML = value;
|
||||
} else {
|
||||
this.populateSlot(slot, value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
populateSlot(slot, value) {
|
||||
if (typeof value == "string" && value.startsWith("http")) {
|
||||
if (slot.localName === "img") slot.src = value;
|
||||
if (slot.localName === "a") slot.href = value;
|
||||
} else {
|
||||
slot.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
handleKey(object, key) {
|
||||
const parsedKeyInt = parseFloat(key);
|
||||
|
||||
if (Number.isNaN(parsedKeyInt)) {
|
||||
return object[key];
|
||||
}
|
||||
|
||||
return object[parsedKeyInt];
|
||||
}
|
||||
|
||||
getValue(string, data) {
|
||||
let keys = string.trim().split(/\.|\[|\]/g);
|
||||
keys = keys.filter((string) => string.length);
|
||||
|
||||
const value = keys.reduce(
|
||||
(object, key) => this.handleKey(object, key),
|
||||
data
|
||||
);
|
||||
return value;
|
||||
}
|
||||
|
||||
get template() {
|
||||
return document
|
||||
.getElementById(
|
||||
this.getAttribute("template") || `${this.localName}-template`
|
||||
)
|
||||
.content.cloneNode(true);
|
||||
}
|
||||
|
||||
get slots() {
|
||||
return this.querySelectorAll("[data-key]");
|
||||
}
|
||||
|
||||
get link() {
|
||||
return this.querySelector("a").href;
|
||||
}
|
||||
|
||||
get linkData() {
|
||||
const url = new URL(this.link);
|
||||
const paths = url.pathname.split("/").filter((string) => string.length);
|
||||
return {
|
||||
url: this.link,
|
||||
hostname: url.hostname,
|
||||
username: paths.find((path) => path.startsWith("@")),
|
||||
postId: paths.find((path) => !path.startsWith("@"))
|
||||
};
|
||||
}
|
||||
|
||||
get endpoint() {
|
||||
return `https://${this.linkData.hostname}/api/v1/statuses/${this.linkData.postId}`;
|
||||
}
|
||||
|
||||
get data() {
|
||||
return fetch(this.endpoint).then((response) => response.json());
|
||||
}
|
||||
}
|
||||
|
||||
MastodonPost.register();
|
File diff suppressed because it is too large
Load diff
|
@ -1,48 +0,0 @@
|
|||
class SelectPagination extends HTMLElement {
|
||||
static register(tagName = 'select-pagination') {
|
||||
if ("customElements" in window) customElements.define(tagName, this)
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['data-base-index']
|
||||
}
|
||||
|
||||
get baseIndex() {
|
||||
return this.getAttribute('data-base-index') || 0
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this.shadowRoot) return
|
||||
|
||||
this.attachShadow({ mode: 'open' }).appendChild(document.createElement('slot'))
|
||||
|
||||
const uriSegments = window.location.pathname.split('/').filter(Boolean)
|
||||
let pageNumber = this.extractPageNumber(uriSegments) || 0
|
||||
|
||||
this.control = this.querySelector('select')
|
||||
this.control.value = pageNumber
|
||||
this.control.addEventListener('change', (event) => {
|
||||
pageNumber = parseInt(event.target.value)
|
||||
const updatedUrlSegments = this.updateUrlSegments(uriSegments, pageNumber)
|
||||
window.location.href = `${window.location.origin}/${updatedUrlSegments.join('/')}`
|
||||
})
|
||||
}
|
||||
|
||||
extractPageNumber(segments) {
|
||||
const lastSegment = segments[segments.length - 1]
|
||||
return !isNaN(lastSegment) ? parseInt(lastSegment) : null
|
||||
}
|
||||
|
||||
updateUrlSegments(segments, pageNumber) {
|
||||
if (!isNaN(segments[segments.length - 1])) {
|
||||
segments[segments.length - 1] = pageNumber.toString()
|
||||
} else {
|
||||
segments.push(pageNumber.toString())
|
||||
}
|
||||
|
||||
if (pageNumber === parseInt(this.baseIndex)) segments.pop()
|
||||
return segments
|
||||
}
|
||||
}
|
||||
|
||||
SelectPagination.register()
|
|
@ -1,45 +0,0 @@
|
|||
class ThemeToggle extends HTMLElement {
|
||||
static tagName = 'theme-toggle'
|
||||
|
||||
static register(tagName = this.tagName, registry = globalThis.customElements) {
|
||||
registry.define(tagName, this)
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this.shadowRoot) return
|
||||
this.attachShadow({ mode: 'open' }).appendChild(document.createElement('slot'))
|
||||
this.root = document.documentElement
|
||||
this.button = this.querySelector('button')
|
||||
this.prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
this.currentTheme = sessionStorage.getItem('theme')
|
||||
|
||||
this.setTheme()
|
||||
|
||||
this.button.addEventListener('click', () => this.toggleTheme())
|
||||
this.prefersDarkScheme.addEventListener('change', (event) => this.onPreferredColorSchemeChange(event))
|
||||
}
|
||||
|
||||
setTheme() {
|
||||
if (!this.currentTheme) {
|
||||
this.currentTheme = this.prefersDarkScheme.matches ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
this.theme = this.currentTheme
|
||||
this.root.setAttribute('data-theme', this.theme)
|
||||
}
|
||||
|
||||
toggleTheme() {
|
||||
this.currentTheme = this.currentTheme === 'dark' ? 'light' : 'dark'
|
||||
sessionStorage.setItem('theme', this.currentTheme)
|
||||
this.setTheme()
|
||||
}
|
||||
|
||||
onPreferredColorSchemeChange(event) {
|
||||
if (!sessionStorage.getItem('theme')) {
|
||||
this.currentTheme = event.matches ? 'dark' : 'light'
|
||||
this.setTheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ThemeToggle.register()
|
|
@ -1,547 +0,0 @@
|
|||
// https://developers.google.com/youtube/iframe_api_reference
|
||||
|
||||
const EMBED_BASE = 'https://www.youtube.com/embed';
|
||||
const API_URL = 'https://www.youtube.com/iframe_api';
|
||||
const API_GLOBAL = 'YT';
|
||||
const API_GLOBAL_READY = 'onYouTubeIframeAPIReady';
|
||||
const MATCH_SRC =
|
||||
/(?:youtu\.be\/|youtube\.com\/(?:shorts\/|embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})/;
|
||||
|
||||
function getTemplateHTML(attrs) {
|
||||
const iframeAttrs = {
|
||||
src: serializeIframeUrl(attrs),
|
||||
frameborder: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
allow: 'accelerometer; fullscreen; autoplay; encrypted-media; gyroscope; picture-in-picture',
|
||||
};
|
||||
|
||||
return /*html*/`
|
||||
<style>
|
||||
:host {
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
min-width: 300px;
|
||||
min-height: 150px;
|
||||
}
|
||||
iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
<iframe${serializeAttributes(iframeAttrs)}></iframe>
|
||||
`;
|
||||
}
|
||||
|
||||
function serializeIframeUrl(attrs) {
|
||||
if (!attrs.src) return;
|
||||
|
||||
const matches = attrs.src.match(MATCH_SRC);
|
||||
const srcId = matches && matches[1];
|
||||
|
||||
const params = {
|
||||
// ?controls=true is enabled by default in the iframe
|
||||
controls: attrs.controls === '' ? null : 0,
|
||||
autoplay: attrs.autoplay,
|
||||
loop: attrs.loop,
|
||||
mute: attrs.muted,
|
||||
playsinline: attrs.playsinline,
|
||||
preload: attrs.preload ?? 'metadata',
|
||||
// origin: globalThis.location?.origin,
|
||||
enablejsapi: 1,
|
||||
showinfo: 0,
|
||||
rel: 0,
|
||||
iv_load_policy: 3,
|
||||
modestbranding: 1,
|
||||
};
|
||||
|
||||
return `${EMBED_BASE}/${srcId}?${serialize(params)}`;
|
||||
}
|
||||
|
||||
class YoutubeVideoElement extends (globalThis.HTMLElement ?? class {}) {
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
static shadowRootOptions = { mode: 'open' };
|
||||
static observedAttributes = [
|
||||
'autoplay',
|
||||
'controls',
|
||||
'crossorigin',
|
||||
'loop',
|
||||
'muted',
|
||||
'playsinline',
|
||||
'poster',
|
||||
'preload',
|
||||
'src',
|
||||
];
|
||||
|
||||
loadComplete = new PublicPromise();
|
||||
#loadRequested;
|
||||
#hasLoaded;
|
||||
#readyState = 0;
|
||||
#seeking = false;
|
||||
#seekComplete;
|
||||
isLoaded = false;
|
||||
|
||||
async load() {
|
||||
if (this.#loadRequested) return;
|
||||
|
||||
if (!this.shadowRoot) {
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
if (this.#hasLoaded) {
|
||||
this.loadComplete = new PublicPromise();
|
||||
this.isLoaded = false;
|
||||
}
|
||||
this.#hasLoaded = true;
|
||||
|
||||
// Wait 1 tick to allow other attributes to be set.
|
||||
await (this.#loadRequested = Promise.resolve());
|
||||
this.#loadRequested = null;
|
||||
|
||||
this.#readyState = 0;
|
||||
this.dispatchEvent(new Event('emptied'));
|
||||
|
||||
let oldApi = this.api;
|
||||
this.api = null;
|
||||
|
||||
if (!this.src) {
|
||||
// Removes the <iframe> containing the player.
|
||||
oldApi?.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new Event('loadstart'));
|
||||
|
||||
let iframe = this.shadowRoot.querySelector('iframe');
|
||||
let attrs = namedNodeMapToObject(this.attributes);
|
||||
|
||||
if (!iframe?.src || iframe.src !== serializeIframeUrl(attrs)) {
|
||||
this.shadowRoot.innerHTML = getTemplateHTML(attrs);
|
||||
iframe = this.shadowRoot.querySelector('iframe');
|
||||
}
|
||||
|
||||
const YT = await loadScript(API_URL, API_GLOBAL, API_GLOBAL_READY);
|
||||
this.api = new YT.Player(iframe, {
|
||||
events: {
|
||||
onReady: () => {
|
||||
this.#readyState = 1; // HTMLMediaElement.HAVE_METADATA
|
||||
this.dispatchEvent(new Event('loadedmetadata'));
|
||||
this.dispatchEvent(new Event('durationchange'));
|
||||
this.dispatchEvent(new Event('volumechange'));
|
||||
this.dispatchEvent(new Event('loadcomplete'));
|
||||
this.isLoaded = true;
|
||||
this.loadComplete.resolve();
|
||||
},
|
||||
onError: (error) => console.error(error),
|
||||
},
|
||||
});
|
||||
|
||||
/* onStateChange
|
||||
-1 (unstarted)
|
||||
0 (ended)
|
||||
1 (playing)
|
||||
2 (paused)
|
||||
3 (buffering)
|
||||
5 (video cued).
|
||||
*/
|
||||
|
||||
let playFired = false;
|
||||
this.api.addEventListener('onStateChange', (event) => {
|
||||
const state = event.data;
|
||||
if (
|
||||
state === YT.PlayerState.PLAYING ||
|
||||
state === YT.PlayerState.BUFFERING
|
||||
) {
|
||||
if (!playFired) {
|
||||
playFired = true;
|
||||
this.dispatchEvent(new Event('play'));
|
||||
}
|
||||
}
|
||||
|
||||
if (state === YT.PlayerState.PLAYING) {
|
||||
if (this.seeking) {
|
||||
this.#seeking = false;
|
||||
this.#seekComplete?.resolve();
|
||||
this.dispatchEvent(new Event('seeked'));
|
||||
}
|
||||
this.#readyState = 3; // HTMLMediaElement.HAVE_FUTURE_DATA
|
||||
this.dispatchEvent(new Event('playing'));
|
||||
} else if (state === YT.PlayerState.PAUSED) {
|
||||
const diff = Math.abs(this.currentTime - lastCurrentTime);
|
||||
if (!this.seeking && diff > 0.1) {
|
||||
this.#seeking = true;
|
||||
this.dispatchEvent(new Event('seeking'));
|
||||
}
|
||||
playFired = false;
|
||||
this.dispatchEvent(new Event('pause'));
|
||||
}
|
||||
if (state === YT.PlayerState.ENDED) {
|
||||
playFired = false;
|
||||
this.dispatchEvent(new Event('pause'));
|
||||
this.dispatchEvent(new Event('ended'));
|
||||
|
||||
if (this.loop) {
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.api.addEventListener('onPlaybackRateChange', () => {
|
||||
this.dispatchEvent(new Event('ratechange'));
|
||||
});
|
||||
|
||||
this.api.addEventListener('onVolumeChange', () => {
|
||||
this.dispatchEvent(new Event('volumechange'));
|
||||
});
|
||||
|
||||
this.api.addEventListener('onVideoProgress', () => {
|
||||
this.dispatchEvent(new Event('timeupdate'));
|
||||
});
|
||||
|
||||
await this.loadComplete;
|
||||
|
||||
let lastCurrentTime = 0;
|
||||
setInterval(() => {
|
||||
const diff = Math.abs(this.currentTime - lastCurrentTime);
|
||||
const bufferedEnd = this.buffered.end(this.buffered.length - 1);
|
||||
if (this.seeking && bufferedEnd > 0.1) {
|
||||
this.#seeking = false;
|
||||
this.#seekComplete?.resolve();
|
||||
this.dispatchEvent(new Event('seeked'));
|
||||
} else if (!this.seeking && diff > 0.1) {
|
||||
this.#seeking = true;
|
||||
this.dispatchEvent(new Event('seeking'));
|
||||
}
|
||||
lastCurrentTime = this.currentTime;
|
||||
}, 50);
|
||||
|
||||
let lastBufferedEnd;
|
||||
const progressInterval = setInterval(() => {
|
||||
const bufferedEnd = this.buffered.end(this.buffered.length - 1);
|
||||
if (bufferedEnd >= this.duration) {
|
||||
clearInterval(progressInterval);
|
||||
this.#readyState = 4; // HTMLMediaElement.HAVE_ENOUGH_DATA
|
||||
}
|
||||
if (lastBufferedEnd != bufferedEnd) {
|
||||
lastBufferedEnd = bufferedEnd;
|
||||
this.dispatchEvent(new Event('progress'));
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async attributeChangedCallback(attrName, oldValue, newValue) {
|
||||
if (oldValue === newValue) return;
|
||||
|
||||
// This is required to come before the await for resolving loadComplete.
|
||||
switch (attrName) {
|
||||
case 'src':
|
||||
case 'autoplay':
|
||||
case 'controls':
|
||||
case 'loop':
|
||||
case 'playsinline': {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async play() {
|
||||
this.#seekComplete = null;
|
||||
await this.loadComplete;
|
||||
// yt.playVideo doesn't return a play promise.
|
||||
this.api?.playVideo();
|
||||
return createPlayPromise(this);
|
||||
}
|
||||
|
||||
async pause() {
|
||||
await this.loadComplete;
|
||||
return this.api?.pauseVideo();
|
||||
}
|
||||
|
||||
get seeking() {
|
||||
return this.#seeking;
|
||||
}
|
||||
|
||||
get readyState() {
|
||||
return this.#readyState;
|
||||
}
|
||||
|
||||
// If the getter from SuperVideoElement is overridden, it's required to define
|
||||
// the setter again too unless it's a read only property! It's a JS thing.
|
||||
|
||||
get src() {
|
||||
return this.getAttribute('src');
|
||||
}
|
||||
|
||||
set src(val) {
|
||||
if (this.src == val) return;
|
||||
this.setAttribute('src', val);
|
||||
}
|
||||
|
||||
/* onStateChange
|
||||
-1 (unstarted)
|
||||
0 (ended)
|
||||
1 (playing)
|
||||
2 (paused)
|
||||
3 (buffering)
|
||||
5 (video cued).
|
||||
*/
|
||||
|
||||
get paused() {
|
||||
if (!this.isLoaded) return !this.autoplay;
|
||||
return [-1, 0, 2, 5].includes(this.api?.getPlayerState?.());
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return this.api?.getDuration?.() ?? NaN;
|
||||
}
|
||||
|
||||
get autoplay() {
|
||||
return this.hasAttribute('autoplay');
|
||||
}
|
||||
|
||||
set autoplay(val) {
|
||||
if (this.autoplay == val) return;
|
||||
this.toggleAttribute('autoplay', Boolean(val));
|
||||
}
|
||||
|
||||
get buffered() {
|
||||
if (!this.isLoaded) return createTimeRanges();
|
||||
const progress =
|
||||
this.api?.getVideoLoadedFraction() * this.api?.getDuration();
|
||||
if (progress > 0) {
|
||||
return createTimeRanges(0, progress);
|
||||
}
|
||||
return createTimeRanges();
|
||||
}
|
||||
|
||||
get controls() {
|
||||
return this.hasAttribute('controls');
|
||||
}
|
||||
|
||||
set controls(val) {
|
||||
if (this.controls == val) return;
|
||||
this.toggleAttribute('controls', Boolean(val));
|
||||
}
|
||||
|
||||
get currentTime() {
|
||||
return this.api?.getCurrentTime?.() ?? 0;
|
||||
}
|
||||
|
||||
set currentTime(val) {
|
||||
if (this.currentTime == val) return;
|
||||
this.#seekComplete = new PublicPromise();
|
||||
this.loadComplete.then(() => {
|
||||
this.api?.seekTo(val, true);
|
||||
if (this.paused) {
|
||||
this.#seekComplete?.then(() => {
|
||||
if (!this.#seekComplete) return;
|
||||
this.api?.pauseVideo();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
set defaultMuted(val) {
|
||||
if (this.defaultMuted == val) return;
|
||||
this.toggleAttribute('muted', Boolean(val));
|
||||
}
|
||||
|
||||
get defaultMuted() {
|
||||
return this.hasAttribute('muted');
|
||||
}
|
||||
|
||||
get loop() {
|
||||
return this.hasAttribute('loop');
|
||||
}
|
||||
|
||||
set loop(val) {
|
||||
if (this.loop == val) return;
|
||||
this.toggleAttribute('loop', Boolean(val));
|
||||
}
|
||||
|
||||
set muted(val) {
|
||||
if (this.muted == val) return;
|
||||
this.loadComplete.then(() => {
|
||||
val ? this.api?.mute() : this.api?.unMute();
|
||||
});
|
||||
}
|
||||
|
||||
get muted() {
|
||||
if (!this.isLoaded) return this.defaultMuted;
|
||||
return this.api?.isMuted?.();
|
||||
}
|
||||
|
||||
get playbackRate() {
|
||||
return this.api?.getPlaybackRate?.() ?? 1;
|
||||
}
|
||||
|
||||
set playbackRate(val) {
|
||||
if (this.playbackRate == val) return;
|
||||
this.loadComplete.then(() => {
|
||||
this.api?.setPlaybackRate(val);
|
||||
});
|
||||
}
|
||||
|
||||
get playsInline() {
|
||||
return this.hasAttribute('playsinline');
|
||||
}
|
||||
|
||||
set playsInline(val) {
|
||||
if (this.playsInline == val) return;
|
||||
this.toggleAttribute('playsinline', Boolean(val));
|
||||
}
|
||||
|
||||
get poster() {
|
||||
return this.getAttribute('poster');
|
||||
}
|
||||
|
||||
set poster(val) {
|
||||
if (this.poster == val) return;
|
||||
this.setAttribute('poster', `${val}`);
|
||||
}
|
||||
|
||||
set volume(val) {
|
||||
if (this.volume == val) return;
|
||||
this.loadComplete.then(() => {
|
||||
this.api?.setVolume(val * 100);
|
||||
});
|
||||
}
|
||||
|
||||
get volume() {
|
||||
if (!this.isLoaded) return 1;
|
||||
return this.api?.getVolume() / 100;
|
||||
}
|
||||
}
|
||||
|
||||
function serializeAttributes(attrs) {
|
||||
let html = '';
|
||||
for (const key in attrs) {
|
||||
const value = attrs[key];
|
||||
if (value === '') html += ` ${key}`;
|
||||
else html += ` ${key}="${value}"`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function serialize(props) {
|
||||
return String(new URLSearchParams(boolToBinary(props)));
|
||||
}
|
||||
|
||||
function boolToBinary(props) {
|
||||
let p = {};
|
||||
for (let key in props) {
|
||||
let val = props[key];
|
||||
if (val === true || val === '') p[key] = 1;
|
||||
else if (val === false) p[key] = 0;
|
||||
else if (val != null) p[key] = val;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
function namedNodeMapToObject(namedNodeMap) {
|
||||
let obj = {};
|
||||
for (let attr of namedNodeMap) {
|
||||
obj[attr.name] = attr.value;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
const loadScriptCache = {};
|
||||
async function loadScript(src, globalName, readyFnName) {
|
||||
if (loadScriptCache[src]) return loadScriptCache[src];
|
||||
if (globalName && self[globalName]) {
|
||||
await delay(0);
|
||||
return self[globalName];
|
||||
}
|
||||
return (loadScriptCache[src] = new Promise(function (resolve, reject) {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
const ready = () => resolve(self[globalName]);
|
||||
if (readyFnName) (self[readyFnName] = ready);
|
||||
script.onload = () => !readyFnName && ready();
|
||||
script.onerror = reject;
|
||||
document.head.append(script);
|
||||
}));
|
||||
}
|
||||
|
||||
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
function promisify(fn) {
|
||||
return (...args) =>
|
||||
new Promise((resolve) => {
|
||||
fn(...args, (...res) => {
|
||||
if (res.length > 1) resolve(res);
|
||||
else resolve(res[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createPlayPromise(player) {
|
||||
return promisify((event, cb) => {
|
||||
let fn;
|
||||
player.addEventListener(
|
||||
event,
|
||||
(fn = () => {
|
||||
player.removeEventListener(event, fn);
|
||||
cb();
|
||||
})
|
||||
);
|
||||
})('playing');
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility to create Promises with convenient public resolve and reject methods.
|
||||
* @return {Promise}
|
||||
*/
|
||||
class PublicPromise extends Promise {
|
||||
constructor(executor = () => {}) {
|
||||
let res, rej;
|
||||
super((resolve, reject) => {
|
||||
executor(resolve, reject);
|
||||
res = resolve;
|
||||
rej = reject;
|
||||
});
|
||||
this.resolve = res;
|
||||
this.reject = rej;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fake `TimeRanges` object.
|
||||
*
|
||||
* A TimeRanges object. This object is normalized, which means that ranges are
|
||||
* ordered, don't overlap, aren't empty, and don't touch (adjacent ranges are
|
||||
* folded into one bigger range).
|
||||
*
|
||||
* @param {(Number|Array)} Start of a single range or an array of ranges
|
||||
* @param {Number} End of a single range
|
||||
* @return {Array}
|
||||
*/
|
||||
function createTimeRanges(start, end) {
|
||||
if (Array.isArray(start)) {
|
||||
return createTimeRangesObj(start);
|
||||
} else if (start == null || end == null || (start === 0 && end === 0)) {
|
||||
return createTimeRangesObj([[0, 0]]);
|
||||
}
|
||||
return createTimeRangesObj([[start, end]]);
|
||||
}
|
||||
|
||||
function createTimeRangesObj(ranges) {
|
||||
Object.defineProperties(ranges, {
|
||||
start: {
|
||||
value: i => ranges[i][0]
|
||||
},
|
||||
end: {
|
||||
value: i => ranges[i][1]
|
||||
}
|
||||
});
|
||||
return ranges;
|
||||
}
|
||||
|
||||
if (globalThis.customElements && !globalThis.customElements.get('youtube-video')) {
|
||||
globalThis.customElements.define('youtube-video', YoutubeVideoElement);
|
||||
}
|
||||
|
||||
export default YoutubeVideoElement;
|
|
@ -1,5 +1,3 @@
|
|||
import MiniSearch from './components/mini-search.js'
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
// menu keyboard controls
|
||||
;(() => {
|
Reference in a new issue