@wu529778790/floating-ad 0.0.0-stage → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,391 @@
1
+ /* @wu529778790/floating-ad v0.1.3 */
2
+ "use strict";
3
+ (() => {
4
+ // src/FloatingAd.ts
5
+ var DEFAULT_THEME = {
6
+ bg: "rgba(255, 255, 255, 0.96)",
7
+ accent: "#333",
8
+ radius: "12px",
9
+ border: "rgba(0, 0, 0, 0.1)"
10
+ };
11
+ var STORAGE_KEY = "floating-ad:closed";
12
+ var MOBILE_QUERY = "(max-width: 767px)";
13
+ function isMobile() {
14
+ var _a, _b, _c;
15
+ if (typeof window === "undefined") return false;
16
+ return (_c = (_b = (_a = window.matchMedia) == null ? void 0 : _a.call(window, MOBILE_QUERY)) == null ? void 0 : _b.matches) != null ? _c : false;
17
+ }
18
+ function hasCloseMark() {
19
+ if (typeof localStorage === "undefined") return false;
20
+ try {
21
+ return localStorage.getItem(STORAGE_KEY) === "1";
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ function setCloseMark() {
27
+ if (typeof localStorage === "undefined") return;
28
+ try {
29
+ localStorage.setItem(STORAGE_KEY, "1");
30
+ } catch {
31
+ }
32
+ }
33
+ function isRenderableAd(ad) {
34
+ var _a;
35
+ return Boolean(ad && typeof ad === "object" && ((_a = ad.src) == null ? void 0 : _a.trim()));
36
+ }
37
+ function resolveOptions(options) {
38
+ var _a, _b, _c, _d, _e, _f;
39
+ const raw = Array.isArray(options.ads) ? options.ads : [];
40
+ return {
41
+ ads: raw.filter(isRenderableAd),
42
+ // 移动端缺省落在左下角(不挡右侧阅读区/返回顶部按钮)
43
+ position: (_a = options.position) != null ? _a : isMobile() ? "left-bottom" : "left-center",
44
+ closePersistence: (_b = options.closePersistence) != null ? _b : false,
45
+ hideOnMobile: (_c = options.hideOnMobile) != null ? _c : false,
46
+ zIndex: (_d = options.zIndex) != null ? _d : 9999,
47
+ adLabel: options.adLabel === void 0 ? false : options.adLabel,
48
+ imageFit: options.imageFit === "contain" ? "contain" : "cover",
49
+ sizeOverrides: {
50
+ width: Number.isFinite(options.width) ? Math.max(80, Number(options.width)) : void 0,
51
+ offset: Number.isFinite(options.offset) ? Math.max(0, Number(options.offset)) : void 0,
52
+ imageSize: Number.isFinite(options.imageSize) ? Math.max(40, Number(options.imageSize)) : void 0
53
+ },
54
+ theme: { ...DEFAULT_THEME, ...(_e = options.theme) != null ? _e : {} },
55
+ themeOverrides: (_f = options.theme) != null ? _f : {},
56
+ onClick: options.onClick
57
+ };
58
+ }
59
+ var FloatingAd = class {
60
+ constructor(options = {}, container = document.body) {
61
+ this.el = null;
62
+ this.closeBtn = null;
63
+ /** 本次渲染的广告列表(与 DOM 上的 data-index 一一对应) */
64
+ this.rendered = [];
65
+ this.handleClose = () => {
66
+ this.close();
67
+ };
68
+ this.handleImageError = (event) => {
69
+ const img = event.target;
70
+ if (!img || img.tagName !== "IMG" || !img.classList.contains("fa-img")) return;
71
+ const media = img.closest(".fa-media");
72
+ if (!media) return;
73
+ media.classList.add("fa-media--failed");
74
+ const item = media.closest(".fa-item");
75
+ if (item && !item.querySelector(".fa-body")) item.classList.add("fa-item--failed");
76
+ };
77
+ this.handleItemClick = (event) => {
78
+ var _a;
79
+ if (!this.opts.onClick) return;
80
+ const target = event.target;
81
+ const item = (_a = target == null ? void 0 : target.closest) == null ? void 0 : _a.call(target, ".fa-item");
82
+ if (!item) return;
83
+ const index = Number(item.dataset.index);
84
+ const ad = this.rendered[index];
85
+ if (!ad) return;
86
+ this.opts.onClick(ad, event);
87
+ };
88
+ this.opts = resolveOptions(options);
89
+ this.container = container;
90
+ if (this.opts.hideOnMobile && isMobile()) return;
91
+ if (this.opts.closePersistence && hasCloseMark()) return;
92
+ this.render();
93
+ }
94
+ /** 是否已挂载到页面 */
95
+ isMounted() {
96
+ return this.el !== null && this.el.isConnected;
97
+ }
98
+ /** 打开(已关闭后再次显示,忽略 localStorage 关闭标记) */
99
+ open() {
100
+ if (this.isMounted()) return;
101
+ this.render();
102
+ }
103
+ /** 关闭浮窗(完全移除)。默认不记住状态,刷新后重新出现 */
104
+ close() {
105
+ if (this.opts.closePersistence) {
106
+ setCloseMark();
107
+ }
108
+ this.destroy();
109
+ }
110
+ /** 从页面移除并解绑 */
111
+ destroy() {
112
+ var _a, _b, _c, _d;
113
+ (_a = this.closeBtn) == null ? void 0 : _a.removeEventListener("click", this.handleClose);
114
+ this.closeBtn = null;
115
+ (_b = this.el) == null ? void 0 : _b.removeEventListener("click", this.handleItemClick);
116
+ (_c = this.el) == null ? void 0 : _c.removeEventListener("error", this.handleImageError, true);
117
+ (_d = this.el) == null ? void 0 : _d.remove();
118
+ this.el = null;
119
+ this.rendered = [];
120
+ }
121
+ /** 用新配置重渲染(保留原容器) */
122
+ update(options) {
123
+ this.destroy();
124
+ this.opts = resolveOptions(options);
125
+ if (this.opts.hideOnMobile && isMobile()) return;
126
+ if (this.opts.closePersistence && hasCloseMark()) return;
127
+ this.render();
128
+ }
129
+ render() {
130
+ var _a;
131
+ const { ads, position, zIndex, themeOverrides, adLabel, imageFit, sizeOverrides } = this.opts;
132
+ if (ads.length === 0) return;
133
+ const root = document.createElement("div");
134
+ root.className = "fa-widget";
135
+ root.dataset.position = position;
136
+ root.style.zIndex = String(zIndex);
137
+ root.style.setProperty("--fa-img-fit", imageFit);
138
+ if (sizeOverrides.width !== void 0) {
139
+ root.style.setProperty("--fa-width", `${sizeOverrides.width}px`);
140
+ }
141
+ if (sizeOverrides.offset !== void 0) {
142
+ root.style.setProperty("--fa-offset", `${sizeOverrides.offset}px`);
143
+ }
144
+ if (sizeOverrides.imageSize !== void 0) {
145
+ root.style.setProperty("--fa-img-size", `${sizeOverrides.imageSize}px`);
146
+ }
147
+ const overrides = [
148
+ ["--fa-bg", themeOverrides.bg],
149
+ ["--fa-accent", themeOverrides.accent],
150
+ ["--fa-radius", themeOverrides.radius],
151
+ ["--fa-border", themeOverrides.border]
152
+ ];
153
+ for (const [key, value] of overrides) {
154
+ if (value !== void 0) root.style.setProperty(key, value);
155
+ }
156
+ const label = adLabel ? escapeHtml(String(adLabel)) : "";
157
+ root.innerHTML = `
158
+ <button class="fa-close" type="button" aria-label="\u5173\u95ED\u5E7F\u544A">${CLOSE_SVG}</button>
159
+ ${ads.map((ad, index) => this.renderItem(ad, index, index === 0 ? label : "")).join('<div class="fa-divider" role="separator"></div>')}
160
+ `;
161
+ this.closeBtn = root.querySelector(".fa-close");
162
+ (_a = this.closeBtn) == null ? void 0 : _a.addEventListener("click", this.handleClose);
163
+ root.addEventListener("click", this.handleItemClick);
164
+ root.addEventListener("error", this.handleImageError, true);
165
+ this.rendered = ads;
166
+ this.container.appendChild(root);
167
+ this.el = root;
168
+ }
169
+ renderItem(ad, index, label) {
170
+ var _a, _b;
171
+ const media = `<span class="fa-media">
172
+ <img class="fa-img" src="${escapeAttr(ad.src)}" alt="${escapeAttr((_b = (_a = ad.alt) != null ? _a : ad.title) != null ? _b : "")}" loading="lazy" decoding="async" />
173
+ ${ad.badge ? `<span class="fa-tag">${escapeHtml(ad.badge)}</span>` : ""}
174
+ ${label ? `<span class="fa-label">${label}</span>` : ""}
175
+ </span>`;
176
+ const body = ad.title || ad.desc ? `<span class="fa-body">
177
+ ${ad.title ? `<span class="fa-title">${escapeHtml(ad.title)}</span>` : ""}
178
+ ${ad.desc ? `<span class="fa-desc">${escapeHtml(ad.desc)}</span>` : ""}
179
+ </span>` : "";
180
+ const inner = `${media}${body}`;
181
+ const href = ad.href ? safeHref(ad.href) : null;
182
+ if (href) {
183
+ const target = ad.target === "_self" ? "_self" : "_blank";
184
+ const rel = target === "_blank" ? ' rel="noopener noreferrer nofollow sponsored"' : ' rel="nofollow sponsored"';
185
+ return `<a class="fa-item" href="${escapeAttr(href)}" target="${target}"${rel} data-index="${index}">${inner}</a>`;
186
+ }
187
+ return `<div class="fa-item" data-index="${index}">${inner}</div>`;
188
+ }
189
+ };
190
+ var CLOSE_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6L6 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
191
+ function safeHref(href) {
192
+ const trimmed = href.trim();
193
+ if (!trimmed) return null;
194
+ const compact = trimmed.replace(/[\s\u0000-\u001f]/g, "").toLowerCase();
195
+ if (/^(javascript|data|vbscript|file):/.test(compact)) return null;
196
+ return trimmed;
197
+ }
198
+ function escapeHtml(value) {
199
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
200
+ }
201
+ function escapeAttr(value) {
202
+ return escapeHtml(value);
203
+ }
204
+
205
+ // src/styles.css
206
+ var styles_default = '.fa-widget {\n --fa-bg: rgba(255, 255, 255, 0.96);\n --fa-accent: #333;\n --fa-radius: 12px;\n --fa-border: rgba(0, 0, 0, 0.1);\n --fa-offset: 16px;\n --fa-width: 150px;\n --fa-img-size: 120px;\n --fa-img-fit: cover;\n\n position: fixed;\n z-index: 9999;\n box-sizing: border-box;\n width: var(--fa-width);\n padding: 14px;\n background: var(--fa-bg);\n border: 1px solid var(--fa-border);\n border-radius: var(--fa-radius);\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,\n "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",\n sans-serif;\n color: var(--fa-accent);\n line-height: 1.5;\n color-scheme: light dark;\n transition: box-shadow 0.2s ease;\n}\n\n.fa-widget:hover {\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);\n}\n\n.fa-widget[data-position="right-bottom"] {\n right: var(--fa-offset);\n bottom: var(--fa-offset);\n}\n\n.fa-widget[data-position="right-top"] {\n right: var(--fa-offset);\n top: var(--fa-offset);\n}\n\n.fa-widget[data-position="left-bottom"] {\n left: var(--fa-offset);\n bottom: var(--fa-offset);\n}\n\n.fa-widget[data-position="left-top"] {\n left: var(--fa-offset);\n top: var(--fa-offset);\n}\n\n.fa-widget[data-position="right-center"] {\n right: var(--fa-offset);\n top: 50%;\n transform: translateY(-50%);\n}\n\n.fa-widget[data-position="left-center"] {\n left: var(--fa-offset);\n top: 50%;\n transform: translateY(-50%);\n}\n\n.fa-close {\n position: absolute;\n top: -2px;\n right: -2px;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n padding: 0;\n margin: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: rgba(0, 0, 0, 0.35);\n cursor: pointer;\n transition: color 0.15s ease, opacity 0.15s ease;\n}\n\n.fa-close:hover {\n color: rgba(0, 0, 0, 0.7);\n}\n\n/* \u89E6\u5C4F\u8BBE\u5907\uFF08\u65E0 hover\uFF09\uFF1A\u5173\u95ED\u6309\u94AE\u5E38\u663E\uFF0C\u4FDD\u8BC1\u53EF\u5173\u95ED */\n.fa-close {\n opacity: 1;\n}\n\n/* \u652F\u6301 hover \u7684\u8BBE\u5907\uFF08\u9F20\u6807\uFF09\uFF1A\u5E73\u65F6\u9690\u85CF\uFF0C\u60AC\u505C\u5361\u7247\u65F6\u6D6E\u73B0 */\n@media (hover: hover) and (pointer: fine) {\n .fa-close {\n opacity: 0;\n pointer-events: none;\n }\n .fa-widget:hover .fa-close,\n .fa-close:focus-visible {\n opacity: 1;\n pointer-events: auto;\n }\n}\n\n/* \u300C\u5E7F\u544A\u300D\u6807\u8BC6\uFF1A\u538B\u5728\u9996\u56FE\u5DE6\u4E0A\u89D2\uFF0C\u4E0D\u5360\u6392\u7248\u884C\u9AD8\n \uFF08\u300A\u5E7F\u544A\u6CD5\u300B\u8981\u6C42\u5E7F\u544A\u53EF\u8BC6\u522B\uFF0C\u6807\u8BC6\u4FDD\u7559\uFF0C\u4F46\u4E0D\u5FC5\u5355\u72EC\u5360\u4E00\u884C\uFF09 */\n.fa-label {\n position: absolute;\n top: 6px;\n left: 6px;\n z-index: 1;\n display: inline-block;\n font-size: 10px;\n line-height: 1;\n letter-spacing: 0.06em;\n padding: 3px 5px;\n border-radius: 4px;\n background: rgba(0, 0, 0, 0.45);\n color: rgba(255, 255, 255, 0.92);\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n pointer-events: none;\n}\n\n.fa-item {\n display: block;\n text-decoration: none;\n color: inherit;\n border-radius: 8px;\n}\n\na.fa-item {\n cursor: pointer;\n}\n\n/* \u56FE\u7247\u56FA\u5B9A\u65B9\u5F62\uFF08\u9ED8\u8BA4 120\xD7120\uFF0C\u4E0E\u53F3\u4FA7\u516C\u4F17\u53F7\u6D6E\u7A97\u7684\u4E8C\u7EF4\u7801\u5C3A\u5BF8\u4E00\u81F4\uFF09 */\n.fa-media {\n position: relative;\n display: block;\n width: var(--fa-img-size, 120px);\n height: var(--fa-img-size, 120px);\n margin: 0 auto;\n overflow: hidden;\n border-radius: 8px;\n background: rgba(0, 0, 0, 0.04);\n}\n\n.fa-img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: var(--fa-img-fit, cover);\n transition: transform 0.25s ease;\n}\n\na.fa-item:hover .fa-img {\n transform: scale(1.04);\n}\n\n/* \u56FE\u7247\u52A0\u8F7D\u5931\u8D25\uFF1A\u9690\u85CF\u7A7A\u56FE\u6846\uFF0C\u53EA\u7559\u6587\u5B57\uFF1B\u8FDE\u6587\u5B57\u90FD\u6CA1\u6709\u5C31\u6574\u6761\u9690\u85CF */\n.fa-media--failed,\n.fa-item--failed {\n display: none;\n}\n\n.fa-tag {\n position: absolute;\n top: 6px;\n right: 6px;\n padding: 1px 6px;\n font-size: 10px;\n line-height: 1.5;\n border-radius: 999px;\n background: var(--fa-accent);\n color: var(--fa-bg);\n}\n\n.fa-body {\n display: block;\n margin-top: 8px;\n text-align: left;\n}\n\n.fa-item > .fa-body:first-child {\n margin-top: 0;\n}\n\n.fa-title {\n display: block;\n font-size: 12px;\n font-weight: 500;\n color: var(--fa-accent);\n}\n\n.fa-desc {\n display: block;\n margin-top: 2px;\n font-size: 11px;\n color: rgba(0, 0, 0, 0.45);\n}\n\n.fa-divider {\n height: 1px;\n margin: 10px 0;\n background: var(--fa-border);\n}\n\n/* \u79FB\u52A8\u7AEF\uFF1A\u6BD4\u684C\u9762\u7565\u5C0F\u4E00\u53F7\uFF08\u5361\u7247 140 / \u56FE\u7247 112 / \u8DDD\u8FB9 12\uFF09\uFF0C\u7F3A\u7701\u843D\u5728\u5DE6\u4E0B\u89D2 */\n@media (max-width: 767px) {\n .fa-widget {\n --fa-width: 140px;\n --fa-img-size: 112px;\n --fa-offset: 12px;\n padding: 13px;\n }\n\n /* \u89E6\u5C4F\u70B9\u6309\u533A\u57DF\u7A0D\u653E\u5927 */\n .fa-close {\n width: 24px;\n height: 24px;\n }\n}\n\n/* \u8DDF\u968F\u7CFB\u7EDF\u6DF1\u8272\u6A21\u5F0F \u2014\u2014 \u5FC5\u987B\u653E\u6587\u4EF6\u672B\u5C3E\uFF0C\u786E\u4FDD\u4F18\u5148\u7EA7\u9AD8\u4E8E\u4E0A\u9762\u7684\u6D45\u8272\u89C4\u5219\n (\u4EC5\u5F71\u54CD\u9ED8\u8BA4\u503C\uFF1B\u7528\u6237\u663E\u5F0F\u8BBE\u7F6E\u7684 --fa-* inline \u53D8\u91CF\u4ECD\u4F18\u5148) */\n@media (prefers-color-scheme: dark) {\n .fa-widget {\n --fa-bg: rgba(28, 28, 30, 0.96);\n --fa-accent: rgba(255, 255, 255, 0.92);\n --fa-border: rgba(255, 255, 255, 0.14);\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);\n }\n\n .fa-widget:hover {\n box-shadow: 0 6px 22px rgba(0, 0, 0, 0.6);\n }\n\n .fa-media {\n background: rgba(255, 255, 255, 0.06);\n }\n\n .fa-close {\n color: rgba(255, 255, 255, 0.4);\n }\n\n .fa-close:hover {\n color: rgba(255, 255, 255, 0.8);\n }\n\n .fa-desc {\n color: rgba(255, 255, 255, 0.55);\n }\n}\n';
207
+
208
+ // src/web-component.ts
209
+ var TAG = "floating-ad";
210
+ var GLOBAL_KEY = "__FLOATING_AD_OPTIONS__";
211
+ var AUTO_FLAG = "__floatingAdAutoInjected__";
212
+ var MOBILE_QUERY2 = "(max-width: 767px)";
213
+ function readGlobal() {
214
+ return window[GLOBAL_KEY];
215
+ }
216
+ function boolAttr(el, name, fallback) {
217
+ const v = el.getAttribute(name);
218
+ if (v === null) return fallback;
219
+ return v === "" || v === "true" || v === "1";
220
+ }
221
+ function numAttr(el, name, fallback) {
222
+ const v = el.getAttribute(name);
223
+ if (v === null || v === "") return fallback;
224
+ const n = Number(v);
225
+ return Number.isFinite(n) ? n : fallback;
226
+ }
227
+ function sizeAttr(el, name) {
228
+ const v = el.getAttribute(name);
229
+ if (v === null || v === "") return void 0;
230
+ const n = Number(v);
231
+ return Number.isFinite(n) ? n : void 0;
232
+ }
233
+ var THEME_ATTRS = [
234
+ ["theme-bg", "bg"],
235
+ ["theme-accent", "accent"],
236
+ ["theme-radius", "radius"],
237
+ ["theme-border", "border"]
238
+ ];
239
+ function parseAdsJson(raw) {
240
+ if (raw === null) return null;
241
+ const trimmed = raw.trim();
242
+ if (!trimmed) return null;
243
+ try {
244
+ const parsed = JSON.parse(trimmed);
245
+ if (Array.isArray(parsed)) return parsed;
246
+ if (parsed && typeof parsed === "object") return [parsed];
247
+ } catch {
248
+ }
249
+ return null;
250
+ }
251
+ function resolveAdLabel(raw, fallback) {
252
+ if (raw === null) return fallback;
253
+ const trimmed = raw.trim();
254
+ if (!trimmed || trimmed === "true") return "\u5E7F\u544A";
255
+ if (/^(false|none|off|0)$/i.test(trimmed)) return false;
256
+ return trimmed;
257
+ }
258
+ var FloatingAdElement = class extends HTMLElement {
259
+ constructor() {
260
+ super();
261
+ this.widget = null;
262
+ this.raf = 0;
263
+ this.shadow = this.attachShadow({ mode: "open" });
264
+ const style = document.createElement("style");
265
+ style.textContent = styles_default;
266
+ this.shadow.appendChild(style);
267
+ }
268
+ static get observedAttributes() {
269
+ return [
270
+ "position",
271
+ "close-persistence",
272
+ "hide-on-mobile",
273
+ "z-index",
274
+ "ad-label",
275
+ "width",
276
+ "offset",
277
+ "image-fit",
278
+ "image-size",
279
+ "ad-src",
280
+ "ad-title",
281
+ "ad-desc",
282
+ "ad-href",
283
+ "ad-target",
284
+ "ad-badge",
285
+ "ads-json",
286
+ ...THEME_ATTRS.map(([attr]) => attr)
287
+ ];
288
+ }
289
+ connectedCallback() {
290
+ this.renderSoon();
291
+ }
292
+ disconnectedCallback() {
293
+ var _a;
294
+ cancelAnimationFrame(this.raf);
295
+ (_a = this.widget) == null ? void 0 : _a.destroy();
296
+ this.widget = null;
297
+ }
298
+ attributeChangedCallback() {
299
+ if (this.isConnected) this.renderSoon();
300
+ }
301
+ /** 手动打开(关闭后再次显示) */
302
+ open() {
303
+ var _a, _b;
304
+ if ((_a = this.widget) == null ? void 0 : _a.isMounted()) return;
305
+ (_b = this.widget) == null ? void 0 : _b.destroy();
306
+ this.widget = new FloatingAd(this.buildOptions(), this.shadow);
307
+ }
308
+ /** 关闭浮窗 */
309
+ close() {
310
+ var _a;
311
+ (_a = this.widget) == null ? void 0 : _a.close();
312
+ }
313
+ /** 以当前属性 + 全局配置重建实例 */
314
+ render() {
315
+ var _a, _b, _c;
316
+ if (!this.isConnected) return;
317
+ const opts = this.buildOptions();
318
+ if (opts.hideOnMobile && ((_b = (_a = window.matchMedia) == null ? void 0 : _a.call(window, MOBILE_QUERY2)) == null ? void 0 : _b.matches)) return;
319
+ (_c = this.widget) == null ? void 0 : _c.destroy();
320
+ this.widget = new FloatingAd(opts, this.shadow);
321
+ }
322
+ renderSoon() {
323
+ cancelAnimationFrame(this.raf);
324
+ this.raf = requestAnimationFrame(() => this.render());
325
+ }
326
+ buildOptions() {
327
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
328
+ const global = (_a = readGlobal()) != null ? _a : {};
329
+ const get = (name) => this.getAttribute(name);
330
+ const theme = {};
331
+ for (const [attr, key] of THEME_ATTRS) {
332
+ const v = get(attr);
333
+ if (v !== null) theme[key] = v;
334
+ }
335
+ const jsonAds = parseAdsJson(get("ads-json"));
336
+ const single = this.readSingleAd();
337
+ const ads = jsonAds != null ? jsonAds : single ? [single] : global.ads;
338
+ const fit = get("image-fit");
339
+ const imageFit = fit === "contain" || fit === "cover" ? fit : global.imageFit;
340
+ return {
341
+ ...global,
342
+ ads,
343
+ position: (_b = get("position")) != null ? _b : global.position,
344
+ closePersistence: boolAttr(this, "close-persistence", (_c = global.closePersistence) != null ? _c : false),
345
+ hideOnMobile: boolAttr(this, "hide-on-mobile", (_d = global.hideOnMobile) != null ? _d : false),
346
+ zIndex: numAttr(this, "z-index", (_e = global.zIndex) != null ? _e : 9999),
347
+ width: (_f = sizeAttr(this, "width")) != null ? _f : global.width,
348
+ offset: (_g = sizeAttr(this, "offset")) != null ? _g : global.offset,
349
+ imageFit,
350
+ imageSize: (_h = sizeAttr(this, "image-size")) != null ? _h : global.imageSize,
351
+ adLabel: resolveAdLabel(get("ad-label"), (_i = global.adLabel) != null ? _i : false),
352
+ theme: { ...(_j = global.theme) != null ? _j : {}, ...theme }
353
+ };
354
+ }
355
+ /** 单条广告的扁平静态属性(ad-src / ad-title / ...);没有 ad-src 时返回 null */
356
+ readSingleAd() {
357
+ const src = this.getAttribute("ad-src");
358
+ if (src === null || !src.trim()) return null;
359
+ const title = this.getAttribute("ad-title");
360
+ const desc = this.getAttribute("ad-desc");
361
+ const href = this.getAttribute("ad-href");
362
+ const badge = this.getAttribute("ad-badge");
363
+ const target = this.getAttribute("ad-target");
364
+ const item = { src };
365
+ if (title !== null) item.title = title;
366
+ if (desc !== null) item.desc = desc;
367
+ if (href !== null) item.href = href;
368
+ if (badge !== null) item.badge = badge;
369
+ if (target === "_self" || target === "_blank") item.target = target;
370
+ return item;
371
+ }
372
+ };
373
+ function autoInit() {
374
+ const doc = document.documentElement;
375
+ if ((doc == null ? void 0 : doc.getAttribute("data-fa-auto")) === "false") return;
376
+ if (document.querySelector(TAG)) return;
377
+ if (window[AUTO_FLAG]) return;
378
+ if (!readGlobal()) return;
379
+ window[AUTO_FLAG] = true;
380
+ const el = document.createElement(TAG);
381
+ document.body.appendChild(el);
382
+ }
383
+ if (!customElements.get(TAG)) {
384
+ customElements.define(TAG, FloatingAdElement);
385
+ if (document.readyState === "loading") {
386
+ document.addEventListener("DOMContentLoaded", autoInit);
387
+ } else {
388
+ autoInit();
389
+ }
390
+ }
391
+ })();
@@ -0,0 +1,88 @@
1
+ type Position = 'right-bottom' | 'right-top' | 'right-center' | 'left-bottom' | 'left-top' | 'left-center';
2
+ /** 单条广告 */
3
+ interface FloatingAdItem {
4
+ /** 广告图片地址(必填;没有图片的条目不会渲染) */
5
+ src: string;
6
+ /** 标题 */
7
+ title?: string;
8
+ /** 副文案 / 卖点 */
9
+ desc?: string;
10
+ /** 点击跳转链接(可选,不传则不可点击) */
11
+ href?: string;
12
+ /** 跳转方式,默认 `_blank`(仅在传了 href 时生效) */
13
+ target?: '_blank' | '_self';
14
+ /** 图片 alt,缺省使用标题 */
15
+ alt?: string;
16
+ /** 图片右上角角标文案,如「限时」 */
17
+ badge?: string;
18
+ }
19
+ interface FloatingAdTheme {
20
+ /** 浮窗背景色 */
21
+ bg?: string;
22
+ /** 强调色(标题、角标) */
23
+ accent?: string;
24
+ /** 圆角大小 */
25
+ radius?: string;
26
+ /** 边框颜色 */
27
+ border?: string;
28
+ }
29
+ interface FloatingAdOptions {
30
+ /** 广告列表(按数组顺序自上而下排列;为空时不渲染浮窗) */
31
+ ads?: FloatingAdItem[];
32
+ /** 浮窗位置,缺省:桌面 left-center / 移动端(<768px)left-bottom */
33
+ position?: Position;
34
+ /** 关闭后是否记住状态(写入 localStorage),默认 false —— false 时刷新页面必然重新出现 */
35
+ closePersistence?: boolean;
36
+ /** 移动端(<768px)是否隐藏,默认 false(移动端展示更小一号的浮窗) */
37
+ hideOnMobile?: boolean;
38
+ /** 浮窗 z-index,默认 9999 */
39
+ zIndex?: number;
40
+ /** 「广告」标识文案(压在首图左上角),默认 false 不显示;传字符串则显示该文案 */
41
+ adLabel?: string | false;
42
+ /** 浮窗宽度(px),缺省:桌面 150 / 移动端 140 */
43
+ width?: number;
44
+ /** 浮窗距视口边缘距离(px),缺省:桌面 16 / 移动端 12 */
45
+ offset?: number;
46
+ /** 图片填充方式,默认 cover */
47
+ imageFit?: 'cover' | 'contain';
48
+ /** 广告图片边长(px,正方形),缺省:桌面 120 / 移动端 112 */
49
+ imageSize?: number;
50
+ /** 主题(映射为 CSS 变量,也可直接覆盖 --fa-* 变量) */
51
+ theme?: FloatingAdTheme;
52
+ /** 点击广告回调(不影响默认跳转行为) */
53
+ onClick?: (ad: FloatingAdItem, event: MouseEvent) => void;
54
+ }
55
+ /**
56
+ * A minimal floating ad slot that pins image / text ads to the left (or right) edge.
57
+ * Zero dependencies. Framework-agnostic. Styleable via --fa-* CSS variables.
58
+ *
59
+ * 没有广告数据时不渲染任何节点(不会留下空白卡片)。
60
+ *
61
+ * @param container 渲染容器,默认 document.body;Web Component 场景传入 shadow root
62
+ */
63
+ declare class FloatingAd {
64
+ private opts;
65
+ private readonly container;
66
+ private el;
67
+ private closeBtn;
68
+ /** 本次渲染的广告列表(与 DOM 上的 data-index 一一对应) */
69
+ private rendered;
70
+ constructor(options?: FloatingAdOptions, container?: HTMLElement | ShadowRoot);
71
+ /** 是否已挂载到页面 */
72
+ isMounted(): boolean;
73
+ /** 打开(已关闭后再次显示,忽略 localStorage 关闭标记) */
74
+ open(): void;
75
+ /** 关闭浮窗(完全移除)。默认不记住状态,刷新后重新出现 */
76
+ close(): void;
77
+ /** 从页面移除并解绑 */
78
+ destroy(): void;
79
+ /** 用新配置重渲染(保留原容器) */
80
+ update(options: FloatingAdOptions): void;
81
+ private render;
82
+ private renderItem;
83
+ private readonly handleClose;
84
+ private readonly handleImageError;
85
+ private readonly handleItemClick;
86
+ }
87
+
88
+ export { FloatingAd, type FloatingAdItem, type FloatingAdOptions, type FloatingAdTheme, type Position, FloatingAd as default };
@@ -0,0 +1,88 @@
1
+ type Position = 'right-bottom' | 'right-top' | 'right-center' | 'left-bottom' | 'left-top' | 'left-center';
2
+ /** 单条广告 */
3
+ interface FloatingAdItem {
4
+ /** 广告图片地址(必填;没有图片的条目不会渲染) */
5
+ src: string;
6
+ /** 标题 */
7
+ title?: string;
8
+ /** 副文案 / 卖点 */
9
+ desc?: string;
10
+ /** 点击跳转链接(可选,不传则不可点击) */
11
+ href?: string;
12
+ /** 跳转方式,默认 `_blank`(仅在传了 href 时生效) */
13
+ target?: '_blank' | '_self';
14
+ /** 图片 alt,缺省使用标题 */
15
+ alt?: string;
16
+ /** 图片右上角角标文案,如「限时」 */
17
+ badge?: string;
18
+ }
19
+ interface FloatingAdTheme {
20
+ /** 浮窗背景色 */
21
+ bg?: string;
22
+ /** 强调色(标题、角标) */
23
+ accent?: string;
24
+ /** 圆角大小 */
25
+ radius?: string;
26
+ /** 边框颜色 */
27
+ border?: string;
28
+ }
29
+ interface FloatingAdOptions {
30
+ /** 广告列表(按数组顺序自上而下排列;为空时不渲染浮窗) */
31
+ ads?: FloatingAdItem[];
32
+ /** 浮窗位置,缺省:桌面 left-center / 移动端(<768px)left-bottom */
33
+ position?: Position;
34
+ /** 关闭后是否记住状态(写入 localStorage),默认 false —— false 时刷新页面必然重新出现 */
35
+ closePersistence?: boolean;
36
+ /** 移动端(<768px)是否隐藏,默认 false(移动端展示更小一号的浮窗) */
37
+ hideOnMobile?: boolean;
38
+ /** 浮窗 z-index,默认 9999 */
39
+ zIndex?: number;
40
+ /** 「广告」标识文案(压在首图左上角),默认 false 不显示;传字符串则显示该文案 */
41
+ adLabel?: string | false;
42
+ /** 浮窗宽度(px),缺省:桌面 150 / 移动端 140 */
43
+ width?: number;
44
+ /** 浮窗距视口边缘距离(px),缺省:桌面 16 / 移动端 12 */
45
+ offset?: number;
46
+ /** 图片填充方式,默认 cover */
47
+ imageFit?: 'cover' | 'contain';
48
+ /** 广告图片边长(px,正方形),缺省:桌面 120 / 移动端 112 */
49
+ imageSize?: number;
50
+ /** 主题(映射为 CSS 变量,也可直接覆盖 --fa-* 变量) */
51
+ theme?: FloatingAdTheme;
52
+ /** 点击广告回调(不影响默认跳转行为) */
53
+ onClick?: (ad: FloatingAdItem, event: MouseEvent) => void;
54
+ }
55
+ /**
56
+ * A minimal floating ad slot that pins image / text ads to the left (or right) edge.
57
+ * Zero dependencies. Framework-agnostic. Styleable via --fa-* CSS variables.
58
+ *
59
+ * 没有广告数据时不渲染任何节点(不会留下空白卡片)。
60
+ *
61
+ * @param container 渲染容器,默认 document.body;Web Component 场景传入 shadow root
62
+ */
63
+ declare class FloatingAd {
64
+ private opts;
65
+ private readonly container;
66
+ private el;
67
+ private closeBtn;
68
+ /** 本次渲染的广告列表(与 DOM 上的 data-index 一一对应) */
69
+ private rendered;
70
+ constructor(options?: FloatingAdOptions, container?: HTMLElement | ShadowRoot);
71
+ /** 是否已挂载到页面 */
72
+ isMounted(): boolean;
73
+ /** 打开(已关闭后再次显示,忽略 localStorage 关闭标记) */
74
+ open(): void;
75
+ /** 关闭浮窗(完全移除)。默认不记住状态,刷新后重新出现 */
76
+ close(): void;
77
+ /** 从页面移除并解绑 */
78
+ destroy(): void;
79
+ /** 用新配置重渲染(保留原容器) */
80
+ update(options: FloatingAdOptions): void;
81
+ private render;
82
+ private renderItem;
83
+ private readonly handleClose;
84
+ private readonly handleImageError;
85
+ private readonly handleItemClick;
86
+ }
87
+
88
+ export { FloatingAd, type FloatingAdItem, type FloatingAdOptions, type FloatingAdTheme, type Position, FloatingAd as default };