initial import: random background api
This commit is contained in:
209
widget.js
Normal file
209
widget.js
Normal file
@@ -0,0 +1,209 @@
|
||||
(() => {
|
||||
const DEFAULT_BASE_URL = (() => {
|
||||
try {
|
||||
return new URL(document.currentScript?.src || window.location.href).origin;
|
||||
} catch {
|
||||
return window.location.origin;
|
||||
}
|
||||
})();
|
||||
|
||||
const STYLE = `
|
||||
:host {
|
||||
display: block;
|
||||
width: var(--rbw-width, 100%);
|
||||
height: var(--rbw-height, 260px);
|
||||
}
|
||||
.frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: var(--rbw-radius, 14px);
|
||||
background: #eef2f7;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: var(--rbw-fit, cover);
|
||||
filter: blur(var(--rbw-blur, 0px));
|
||||
transform: scale(1.04);
|
||||
opacity: 0;
|
||||
transition: opacity 180ms ease;
|
||||
will-change: filter, opacity, transform;
|
||||
}
|
||||
.state {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: #5b657a;
|
||||
font: 13px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.45), rgba(255,255,255,.2));
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
:host([loaded]) .state,
|
||||
:host([error]) .state {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
:host([loaded]) img {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const DEFAULTS = {
|
||||
mode: 'auto',
|
||||
blur: '0',
|
||||
radius: '14px',
|
||||
fit: 'cover',
|
||||
height: '260px',
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
class RandomBackgroundWidget extends HTMLElement {
|
||||
static observedAttributes = ['src', 'mode', 'blur', 'radius', 'fit', 'height', 'width'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this._manifest = null;
|
||||
this._requestSeq = 0;
|
||||
this._boundRefresh = this.refresh.bind(this);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.refresh();
|
||||
window.addEventListener('resize', this._boundRefresh, { passive: true });
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener('resize', this._boundRefresh);
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
if (this.isConnected) {
|
||||
this.render();
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
return (this.getAttribute('src') || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return (this.getAttribute('mode') || DEFAULTS.mode).toLowerCase();
|
||||
}
|
||||
|
||||
get blur() {
|
||||
const raw = Number.parseFloat(this.getAttribute('blur') || DEFAULTS.blur);
|
||||
return Number.isFinite(raw) && raw > 0 ? raw : 0;
|
||||
}
|
||||
|
||||
get radius() {
|
||||
return this.getAttribute('radius') || DEFAULTS.radius;
|
||||
}
|
||||
|
||||
get fit() {
|
||||
return this.getAttribute('fit') || DEFAULTS.fit;
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this.getAttribute('height') || DEFAULTS.height;
|
||||
}
|
||||
|
||||
get width() {
|
||||
return this.getAttribute('width') || DEFAULTS.width;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.shadowRoot) return;
|
||||
this.style.setProperty('--rbw-width', this.width);
|
||||
this.style.setProperty('--rbw-height', this.height);
|
||||
this.style.setProperty('--rbw-radius', this.radius);
|
||||
this.style.setProperty('--rbw-fit', this.fit);
|
||||
this.style.setProperty('--rbw-blur', `${this.blur}px`);
|
||||
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>${STYLE}</style>
|
||||
<div class="frame">
|
||||
<img part="image" alt="random background">
|
||||
<div class="state">加载中</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
const seq = ++this._requestSeq;
|
||||
const img = this.shadowRoot.querySelector('img');
|
||||
const state = this.shadowRoot.querySelector('.state');
|
||||
|
||||
try {
|
||||
if (!this._manifest) {
|
||||
this._manifest = await this.fetchManifest();
|
||||
}
|
||||
if (seq !== this._requestSeq) return;
|
||||
|
||||
const variant = this.resolveVariant();
|
||||
const data = this._manifest?.variants?.[variant] || {};
|
||||
const folder = data.folder || (variant === 'mobile' ? 'mobile-image' : 'desketop-image');
|
||||
const images = Array.isArray(data.images) ? data.images.filter((item) => typeof item === 'string') : [];
|
||||
|
||||
if (!images.length) {
|
||||
throw new Error(`no images in ${folder}`);
|
||||
}
|
||||
|
||||
const filename = images[Math.floor(Math.random() * images.length)];
|
||||
img.src = `${this.baseUrl}/${folder}/${filename}`;
|
||||
img.alt = `random background ${variant}`;
|
||||
this.setAttribute('loaded', '');
|
||||
this.removeAttribute('error');
|
||||
state.textContent = '';
|
||||
} catch (error) {
|
||||
this.removeAttribute('loaded');
|
||||
this.setAttribute('error', '');
|
||||
state.textContent = error instanceof Error ? error.message : 'load failed';
|
||||
}
|
||||
}
|
||||
|
||||
resolveVariant() {
|
||||
if (this.mode === 'mobile' || this.mode === 'desktop') return this.mode;
|
||||
|
||||
const orientation = window.matchMedia('(orientation: portrait)').matches ? 'portrait' : 'landscape';
|
||||
const device = this.detectDevice();
|
||||
|
||||
if (orientation === 'portrait') return 'mobile';
|
||||
if (orientation === 'landscape') return 'desktop';
|
||||
return device;
|
||||
}
|
||||
|
||||
detectDevice() {
|
||||
if (navigator.userAgentData && typeof navigator.userAgentData.mobile === 'boolean') {
|
||||
return navigator.userAgentData.mobile ? 'mobile' : 'desktop';
|
||||
}
|
||||
return /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent) ? 'mobile' : 'desktop';
|
||||
}
|
||||
|
||||
async fetchManifest() {
|
||||
const url = new URL('manifest.json', `${this.baseUrl}/`);
|
||||
const response = await fetch(url.toString(), { mode: 'cors', cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`manifest ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('random-background-widget')) {
|
||||
customElements.define('random-background-widget', RandomBackgroundWidget);
|
||||
}
|
||||
|
||||
window.RandomBackgroundWidget = RandomBackgroundWidget;
|
||||
})();
|
||||
Reference in New Issue
Block a user