@wu529778790/floating-ad 0.0.0-stage → 0.1.2

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,208 @@
1
+ // src/FloatingAd.ts
2
+ var DEFAULT_THEME = {
3
+ bg: "rgba(255, 255, 255, 0.96)",
4
+ accent: "#333",
5
+ radius: "12px",
6
+ border: "rgba(0, 0, 0, 0.1)"
7
+ };
8
+ var STORAGE_KEY = "floating-ad:closed";
9
+ var MOBILE_QUERY = "(max-width: 767px)";
10
+ function isMobile() {
11
+ var _a, _b, _c;
12
+ if (typeof window === "undefined") return false;
13
+ return (_c = (_b = (_a = window.matchMedia) == null ? void 0 : _a.call(window, MOBILE_QUERY)) == null ? void 0 : _b.matches) != null ? _c : false;
14
+ }
15
+ function hasCloseMark() {
16
+ if (typeof localStorage === "undefined") return false;
17
+ try {
18
+ return localStorage.getItem(STORAGE_KEY) === "1";
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+ function setCloseMark() {
24
+ if (typeof localStorage === "undefined") return;
25
+ try {
26
+ localStorage.setItem(STORAGE_KEY, "1");
27
+ } catch {
28
+ }
29
+ }
30
+ function isRenderableAd(ad) {
31
+ var _a;
32
+ return Boolean(ad && typeof ad === "object" && ((_a = ad.src) == null ? void 0 : _a.trim()));
33
+ }
34
+ function resolveOptions(options) {
35
+ var _a, _b, _c, _d, _e, _f;
36
+ const raw = Array.isArray(options.ads) ? options.ads : [];
37
+ return {
38
+ ads: raw.filter(isRenderableAd),
39
+ // 移动端缺省落在左下角(不挡右侧阅读区/返回顶部按钮)
40
+ position: (_a = options.position) != null ? _a : isMobile() ? "left-bottom" : "left-center",
41
+ closePersistence: (_b = options.closePersistence) != null ? _b : false,
42
+ hideOnMobile: (_c = options.hideOnMobile) != null ? _c : false,
43
+ zIndex: (_d = options.zIndex) != null ? _d : 9999,
44
+ adLabel: options.adLabel === void 0 ? false : options.adLabel,
45
+ imageFit: options.imageFit === "contain" ? "contain" : "cover",
46
+ sizeOverrides: {
47
+ width: Number.isFinite(options.width) ? Math.max(80, Number(options.width)) : void 0,
48
+ offset: Number.isFinite(options.offset) ? Math.max(0, Number(options.offset)) : void 0,
49
+ imageSize: Number.isFinite(options.imageSize) ? Math.max(40, Number(options.imageSize)) : void 0
50
+ },
51
+ theme: { ...DEFAULT_THEME, ...(_e = options.theme) != null ? _e : {} },
52
+ themeOverrides: (_f = options.theme) != null ? _f : {},
53
+ onClick: options.onClick
54
+ };
55
+ }
56
+ var FloatingAd = class {
57
+ constructor(options = {}, container = document.body) {
58
+ this.el = null;
59
+ this.closeBtn = null;
60
+ /** 本次渲染的广告列表(与 DOM 上的 data-index 一一对应) */
61
+ this.rendered = [];
62
+ this.handleClose = () => {
63
+ this.close();
64
+ };
65
+ this.handleImageError = (event) => {
66
+ const img = event.target;
67
+ if (!img || img.tagName !== "IMG" || !img.classList.contains("fa-img")) return;
68
+ const media = img.closest(".fa-media");
69
+ if (!media) return;
70
+ media.classList.add("fa-media--failed");
71
+ const item = media.closest(".fa-item");
72
+ if (item && !item.querySelector(".fa-body")) item.classList.add("fa-item--failed");
73
+ };
74
+ this.handleItemClick = (event) => {
75
+ var _a;
76
+ if (!this.opts.onClick) return;
77
+ const target = event.target;
78
+ const item = (_a = target == null ? void 0 : target.closest) == null ? void 0 : _a.call(target, ".fa-item");
79
+ if (!item) return;
80
+ const index = Number(item.dataset.index);
81
+ const ad = this.rendered[index];
82
+ if (!ad) return;
83
+ this.opts.onClick(ad, event);
84
+ };
85
+ this.opts = resolveOptions(options);
86
+ this.container = container;
87
+ if (this.opts.hideOnMobile && isMobile()) return;
88
+ if (this.opts.closePersistence && hasCloseMark()) return;
89
+ this.render();
90
+ }
91
+ /** 是否已挂载到页面 */
92
+ isMounted() {
93
+ return this.el !== null && this.el.isConnected;
94
+ }
95
+ /** 打开(已关闭后再次显示,忽略 localStorage 关闭标记) */
96
+ open() {
97
+ if (this.isMounted()) return;
98
+ this.render();
99
+ }
100
+ /** 关闭浮窗(完全移除)。默认不记住状态,刷新后重新出现 */
101
+ close() {
102
+ if (this.opts.closePersistence) {
103
+ setCloseMark();
104
+ }
105
+ this.destroy();
106
+ }
107
+ /** 从页面移除并解绑 */
108
+ destroy() {
109
+ var _a, _b, _c, _d;
110
+ (_a = this.closeBtn) == null ? void 0 : _a.removeEventListener("click", this.handleClose);
111
+ this.closeBtn = null;
112
+ (_b = this.el) == null ? void 0 : _b.removeEventListener("click", this.handleItemClick);
113
+ (_c = this.el) == null ? void 0 : _c.removeEventListener("error", this.handleImageError, true);
114
+ (_d = this.el) == null ? void 0 : _d.remove();
115
+ this.el = null;
116
+ this.rendered = [];
117
+ }
118
+ /** 用新配置重渲染(保留原容器) */
119
+ update(options) {
120
+ this.destroy();
121
+ this.opts = resolveOptions(options);
122
+ if (this.opts.hideOnMobile && isMobile()) return;
123
+ if (this.opts.closePersistence && hasCloseMark()) return;
124
+ this.render();
125
+ }
126
+ render() {
127
+ var _a;
128
+ const { ads, position, zIndex, themeOverrides, adLabel, imageFit, sizeOverrides } = this.opts;
129
+ if (ads.length === 0) return;
130
+ const root = document.createElement("div");
131
+ root.className = "fa-widget";
132
+ root.dataset.position = position;
133
+ root.style.zIndex = String(zIndex);
134
+ root.style.setProperty("--fa-img-fit", imageFit);
135
+ if (sizeOverrides.width !== void 0) {
136
+ root.style.setProperty("--fa-width", `${sizeOverrides.width}px`);
137
+ }
138
+ if (sizeOverrides.offset !== void 0) {
139
+ root.style.setProperty("--fa-offset", `${sizeOverrides.offset}px`);
140
+ }
141
+ if (sizeOverrides.imageSize !== void 0) {
142
+ root.style.setProperty("--fa-img-size", `${sizeOverrides.imageSize}px`);
143
+ }
144
+ const overrides = [
145
+ ["--fa-bg", themeOverrides.bg],
146
+ ["--fa-accent", themeOverrides.accent],
147
+ ["--fa-radius", themeOverrides.radius],
148
+ ["--fa-border", themeOverrides.border]
149
+ ];
150
+ for (const [key, value] of overrides) {
151
+ if (value !== void 0) root.style.setProperty(key, value);
152
+ }
153
+ const label = adLabel ? escapeHtml(String(adLabel)) : "";
154
+ root.innerHTML = `
155
+ <button class="fa-close" type="button" aria-label="\u5173\u95ED\u5E7F\u544A">${CLOSE_SVG}</button>
156
+ ${ads.map((ad, index) => this.renderItem(ad, index, index === 0 ? label : "")).join('<div class="fa-divider" role="separator"></div>')}
157
+ `;
158
+ this.closeBtn = root.querySelector(".fa-close");
159
+ (_a = this.closeBtn) == null ? void 0 : _a.addEventListener("click", this.handleClose);
160
+ root.addEventListener("click", this.handleItemClick);
161
+ root.addEventListener("error", this.handleImageError, true);
162
+ this.rendered = ads;
163
+ this.container.appendChild(root);
164
+ this.el = root;
165
+ }
166
+ renderItem(ad, index, label) {
167
+ var _a, _b;
168
+ const media = `<span class="fa-media">
169
+ <img class="fa-img" src="${escapeAttr(ad.src)}" alt="${escapeAttr((_b = (_a = ad.alt) != null ? _a : ad.title) != null ? _b : "")}" loading="lazy" decoding="async" />
170
+ ${ad.badge ? `<span class="fa-tag">${escapeHtml(ad.badge)}</span>` : ""}
171
+ ${label ? `<span class="fa-label">${label}</span>` : ""}
172
+ </span>`;
173
+ const body = ad.title || ad.desc ? `<span class="fa-body">
174
+ ${ad.title ? `<span class="fa-title">${escapeHtml(ad.title)}</span>` : ""}
175
+ ${ad.desc ? `<span class="fa-desc">${escapeHtml(ad.desc)}</span>` : ""}
176
+ </span>` : "";
177
+ const inner = `${media}${body}`;
178
+ const href = ad.href ? safeHref(ad.href) : null;
179
+ if (href) {
180
+ const target = ad.target === "_self" ? "_self" : "_blank";
181
+ const rel = target === "_blank" ? ' rel="noopener noreferrer nofollow sponsored"' : ' rel="nofollow sponsored"';
182
+ return `<a class="fa-item" href="${escapeAttr(href)}" target="${target}"${rel} data-index="${index}">${inner}</a>`;
183
+ }
184
+ return `<div class="fa-item" data-index="${index}">${inner}</div>`;
185
+ }
186
+ };
187
+ 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>';
188
+ function safeHref(href) {
189
+ const trimmed = href.trim();
190
+ if (!trimmed) return null;
191
+ const compact = trimmed.replace(/[\s\u0000-\u001f]/g, "").toLowerCase();
192
+ if (/^(javascript|data|vbscript|file):/.test(compact)) return null;
193
+ return trimmed;
194
+ }
195
+ function escapeHtml(value) {
196
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
197
+ }
198
+ function escapeAttr(value) {
199
+ return escapeHtml(value);
200
+ }
201
+
202
+ // src/index.ts
203
+ var index_default = FloatingAd;
204
+ export {
205
+ FloatingAd,
206
+ index_default as default
207
+ };
208
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/FloatingAd.ts","../src/index.ts"],"sourcesContent":["export type Position =\n | 'right-bottom'\n | 'right-top'\n | 'right-center'\n | 'left-bottom'\n | 'left-top'\n | 'left-center'\n\n/** 单条广告 */\nexport interface FloatingAdItem {\n /** 广告图片地址(必填;没有图片的条目不会渲染) */\n src: string\n /** 标题 */\n title?: string\n /** 副文案 / 卖点 */\n desc?: string\n /** 点击跳转链接(可选,不传则不可点击) */\n href?: string\n /** 跳转方式,默认 `_blank`(仅在传了 href 时生效) */\n target?: '_blank' | '_self'\n /** 图片 alt,缺省使用标题 */\n alt?: string\n /** 图片右上角角标文案,如「限时」 */\n badge?: string\n}\n\nexport interface FloatingAdTheme {\n /** 浮窗背景色 */\n bg?: string\n /** 强调色(标题、角标) */\n accent?: string\n /** 圆角大小 */\n radius?: string\n /** 边框颜色 */\n border?: string\n}\n\nexport interface FloatingAdOptions {\n /** 广告列表(按数组顺序自上而下排列;为空时不渲染浮窗) */\n ads?: FloatingAdItem[]\n /** 浮窗位置,缺省:桌面 left-center / 移动端(<768px)left-bottom */\n position?: Position\n /** 关闭后是否记住状态(写入 localStorage),默认 false —— false 时刷新页面必然重新出现 */\n closePersistence?: boolean\n /** 移动端(<768px)是否隐藏,默认 false(移动端展示更小一号的浮窗) */\n hideOnMobile?: boolean\n /** 浮窗 z-index,默认 9999 */\n zIndex?: number\n /** 「广告」标识文案(压在首图左上角),默认 false 不显示;传字符串则显示该文案 */\n adLabel?: string | false\n /** 浮窗宽度(px),缺省:桌面 150 / 移动端 140 */\n width?: number\n /** 浮窗距视口边缘距离(px),缺省:桌面 16 / 移动端 12 */\n offset?: number\n /** 图片填充方式,默认 cover */\n imageFit?: 'cover' | 'contain'\n /** 广告图片边长(px,正方形),缺省:桌面 120 / 移动端 112 */\n imageSize?: number\n /** 主题(映射为 CSS 变量,也可直接覆盖 --fa-* 变量) */\n theme?: FloatingAdTheme\n /** 点击广告回调(不影响默认跳转行为) */\n onClick?: (ad: FloatingAdItem, event: MouseEvent) => void\n}\n\ninterface ResolvedOptions {\n ads: FloatingAdItem[]\n position: Position\n closePersistence: boolean\n hideOnMobile: boolean\n zIndex: number\n adLabel: string | false\n imageFit: 'cover' | 'contain'\n /** 用户显式传入的尺寸子集(未配置时不写 inline,交给 CSS 按桌面/移动端默认值走) */\n sizeOverrides: {\n width?: number\n offset?: number\n imageSize?: number\n }\n theme: Required<FloatingAdTheme>\n /** 用户显式传入的主题子集(未配置的 key 交给 CSS 默认值,可随系统深浅色变化) */\n themeOverrides: FloatingAdTheme\n onClick?: (ad: FloatingAdItem, event: MouseEvent) => void\n}\n\nconst DEFAULT_THEME: Required<FloatingAdTheme> = {\n bg: 'rgba(255, 255, 255, 0.96)',\n accent: '#333',\n radius: '12px',\n border: 'rgba(0, 0, 0, 0.1)'\n}\n\nconst STORAGE_KEY = 'floating-ad:closed'\nconst MOBILE_QUERY = '(max-width: 767px)'\n\nfunction isMobile(): boolean {\n if (typeof window === 'undefined') return false\n return window.matchMedia?.(MOBILE_QUERY)?.matches ?? false\n}\n\nfunction hasCloseMark(): boolean {\n if (typeof localStorage === 'undefined') return false\n try {\n return localStorage.getItem(STORAGE_KEY) === '1'\n } catch {\n return false\n }\n}\n\nfunction setCloseMark(): void {\n if (typeof localStorage === 'undefined') return\n try {\n localStorage.setItem(STORAGE_KEY, '1')\n } catch {\n /* ignore quota / privacy mode */\n }\n}\n\n/** 只有带图片的条目才会渲染(本组件只承载图片广告) */\nfunction isRenderableAd(ad: FloatingAdItem | null | undefined): ad is FloatingAdItem {\n return Boolean(ad && typeof ad === 'object' && ad.src?.trim())\n}\n\nfunction resolveOptions(options: FloatingAdOptions): ResolvedOptions {\n const raw = Array.isArray(options.ads) ? options.ads : []\n return {\n ads: raw.filter(isRenderableAd),\n // 移动端缺省落在左下角(不挡右侧阅读区/返回顶部按钮)\n position: options.position ?? (isMobile() ? 'left-bottom' : 'left-center'),\n closePersistence: options.closePersistence ?? false,\n hideOnMobile: options.hideOnMobile ?? false,\n zIndex: options.zIndex ?? 9999,\n adLabel: options.adLabel === undefined ? false : options.adLabel,\n imageFit: options.imageFit === 'contain' ? 'contain' : 'cover',\n sizeOverrides: {\n width: Number.isFinite(options.width) ? Math.max(80, Number(options.width)) : undefined,\n offset: Number.isFinite(options.offset) ? Math.max(0, Number(options.offset)) : undefined,\n imageSize: Number.isFinite(options.imageSize)\n ? Math.max(40, Number(options.imageSize))\n : undefined\n },\n theme: { ...DEFAULT_THEME, ...(options.theme ?? {}) },\n themeOverrides: options.theme ?? {},\n onClick: options.onClick\n }\n}\n\n/**\n * A minimal floating ad slot that pins image / text ads to the left (or right) edge.\n * Zero dependencies. Framework-agnostic. Styleable via --fa-* CSS variables.\n *\n * 没有广告数据时不渲染任何节点(不会留下空白卡片)。\n *\n * @param container 渲染容器,默认 document.body;Web Component 场景传入 shadow root\n */\nexport class FloatingAd {\n private opts: ResolvedOptions\n private readonly container: HTMLElement | ShadowRoot\n private el: HTMLElement | null = null\n private closeBtn: HTMLButtonElement | null = null\n /** 本次渲染的广告列表(与 DOM 上的 data-index 一一对应) */\n private rendered: FloatingAdItem[] = []\n\n constructor(\n options: FloatingAdOptions = {},\n container: HTMLElement | ShadowRoot = document.body\n ) {\n this.opts = resolveOptions(options)\n this.container = container\n\n if (this.opts.hideOnMobile && isMobile()) return\n if (this.opts.closePersistence && hasCloseMark()) return\n\n this.render()\n }\n\n /** 是否已挂载到页面 */\n isMounted(): boolean {\n return this.el !== null && this.el.isConnected\n }\n\n /** 打开(已关闭后再次显示,忽略 localStorage 关闭标记) */\n open(): void {\n if (this.isMounted()) return\n this.render()\n }\n\n /** 关闭浮窗(完全移除)。默认不记住状态,刷新后重新出现 */\n close(): void {\n if (this.opts.closePersistence) {\n setCloseMark()\n }\n this.destroy()\n }\n\n /** 从页面移除并解绑 */\n destroy(): void {\n this.closeBtn?.removeEventListener('click', this.handleClose)\n this.closeBtn = null\n this.el?.removeEventListener('click', this.handleItemClick)\n this.el?.removeEventListener('error', this.handleImageError, true)\n this.el?.remove()\n this.el = null\n this.rendered = []\n }\n\n /** 用新配置重渲染(保留原容器) */\n update(options: FloatingAdOptions): void {\n this.destroy()\n this.opts = resolveOptions(options)\n\n if (this.opts.hideOnMobile && isMobile()) return\n if (this.opts.closePersistence && hasCloseMark()) return\n\n this.render()\n }\n\n private render(): void {\n const { ads, position, zIndex, themeOverrides, adLabel, imageFit, sizeOverrides } = this.opts\n\n if (ads.length === 0) return\n\n const root = document.createElement('div')\n root.className = 'fa-widget'\n root.dataset.position = position\n root.style.zIndex = String(zIndex)\n root.style.setProperty('--fa-img-fit', imageFit)\n // 尺寸只在用户显式配置时写 inline,其余交给 CSS(桌面 / 移动端各一套默认值)\n if (sizeOverrides.width !== undefined) {\n root.style.setProperty('--fa-width', `${sizeOverrides.width}px`)\n }\n if (sizeOverrides.offset !== undefined) {\n root.style.setProperty('--fa-offset', `${sizeOverrides.offset}px`)\n }\n if (sizeOverrides.imageSize !== undefined) {\n root.style.setProperty('--fa-img-size', `${sizeOverrides.imageSize}px`)\n }\n\n // 仅把用户显式设置的主题写为 inline(用户优先);\n // 未设置的 key 交给 CSS 变量默认值,以便跟随系统深浅色(prefers-color-scheme)\n const overrides = [\n ['--fa-bg', themeOverrides.bg],\n ['--fa-accent', themeOverrides.accent],\n ['--fa-radius', themeOverrides.radius],\n ['--fa-border', themeOverrides.border]\n ] as const\n for (const [key, value] of overrides) {\n if (value !== undefined) root.style.setProperty(key, value)\n }\n\n // 「广告」标识不单独占行:压在首图左上角\n const label = adLabel ? escapeHtml(String(adLabel)) : ''\n\n root.innerHTML = `\n <button class=\"fa-close\" type=\"button\" aria-label=\"关闭广告\">${CLOSE_SVG}</button>\n ${ads\n .map((ad, index) => this.renderItem(ad, index, index === 0 ? label : ''))\n .join('<div class=\"fa-divider\" role=\"separator\"></div>')}\n `\n\n this.closeBtn = root.querySelector<HTMLButtonElement>('.fa-close')\n this.closeBtn?.addEventListener('click', this.handleClose)\n root.addEventListener('click', this.handleItemClick)\n // 图片加载失败时降级:隐藏空图框,仅保留文字;连文字都没有就整条隐藏\n root.addEventListener('error', this.handleImageError, true)\n\n this.rendered = ads\n this.container.appendChild(root)\n this.el = root\n }\n\n private renderItem(ad: FloatingAdItem, index: number, label: string): string {\n const media = `<span class=\"fa-media\">\n <img class=\"fa-img\" src=\"${escapeAttr(ad.src)}\" alt=\"${escapeAttr(ad.alt ?? ad.title ?? '')}\" loading=\"lazy\" decoding=\"async\" />\n ${ad.badge ? `<span class=\"fa-tag\">${escapeHtml(ad.badge)}</span>` : ''}\n ${label ? `<span class=\"fa-label\">${label}</span>` : ''}\n </span>`\n\n const body =\n ad.title || ad.desc\n ? `<span class=\"fa-body\">\n ${ad.title ? `<span class=\"fa-title\">${escapeHtml(ad.title)}</span>` : ''}\n ${ad.desc ? `<span class=\"fa-desc\">${escapeHtml(ad.desc)}</span>` : ''}\n </span>`\n : ''\n\n const inner = `${media}${body}`\n\n const href = ad.href ? safeHref(ad.href) : null\n if (href) {\n const target = ad.target === '_self' ? '_self' : '_blank'\n const rel =\n target === '_blank'\n ? ' rel=\"noopener noreferrer nofollow sponsored\"'\n : ' rel=\"nofollow sponsored\"'\n return `<a class=\"fa-item\" href=\"${escapeAttr(href)}\" target=\"${target}\"${rel} data-index=\"${index}\">${inner}</a>`\n }\n\n return `<div class=\"fa-item\" data-index=\"${index}\">${inner}</div>`\n }\n\n private readonly handleClose = (): void => {\n this.close()\n }\n\n private readonly handleImageError = (event: Event): void => {\n const img = event.target as HTMLElement | null\n if (!img || img.tagName !== 'IMG' || !img.classList.contains('fa-img')) return\n const media = img.closest('.fa-media')\n if (!media) return\n media.classList.add('fa-media--failed')\n const item = media.closest('.fa-item')\n if (item && !item.querySelector('.fa-body')) item.classList.add('fa-item--failed')\n }\n\n private readonly handleItemClick = (event: Event): void => {\n if (!this.opts.onClick) return\n const target = event.target as Element | null\n const item = target?.closest?.('.fa-item') as HTMLElement | null\n if (!item) return\n const index = Number(item.dataset.index)\n const ad = this.rendered[index]\n if (!ad) return\n this.opts.onClick(ad, event as MouseEvent)\n }\n}\n\nconst CLOSE_SVG =\n '<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>'\n\n/** 过滤危险协议(javascript: / data: / vbscript: 等),返回可用链接或 null */\nfunction safeHref(href: string): string | null {\n const trimmed = href.trim()\n if (!trimmed) return null\n const compact = trimmed.replace(/[\\s\\u0000-\\u001f]/g, '').toLowerCase()\n if (/^(javascript|data|vbscript|file):/.test(compact)) return null\n return trimmed\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\n}\n\nfunction escapeAttr(value: string): string {\n return escapeHtml(value)\n}\n","import { FloatingAd } from './FloatingAd'\n\nexport { FloatingAd }\nexport default FloatingAd\nexport type { FloatingAdOptions, FloatingAdItem, FloatingAdTheme, Position } from './FloatingAd'\n"],"mappings":"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","names":[]}
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ FloatingAd: () => FloatingAd,
24
+ default: () => index_default
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/FloatingAd.ts
29
+ var DEFAULT_THEME = {
30
+ bg: "rgba(255, 255, 255, 0.96)",
31
+ accent: "#333",
32
+ radius: "12px",
33
+ border: "rgba(0, 0, 0, 0.1)"
34
+ };
35
+ var STORAGE_KEY = "floating-ad:closed";
36
+ var MOBILE_QUERY = "(max-width: 767px)";
37
+ function isMobile() {
38
+ var _a, _b, _c;
39
+ if (typeof window === "undefined") return false;
40
+ return (_c = (_b = (_a = window.matchMedia) == null ? void 0 : _a.call(window, MOBILE_QUERY)) == null ? void 0 : _b.matches) != null ? _c : false;
41
+ }
42
+ function hasCloseMark() {
43
+ if (typeof localStorage === "undefined") return false;
44
+ try {
45
+ return localStorage.getItem(STORAGE_KEY) === "1";
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+ function setCloseMark() {
51
+ if (typeof localStorage === "undefined") return;
52
+ try {
53
+ localStorage.setItem(STORAGE_KEY, "1");
54
+ } catch {
55
+ }
56
+ }
57
+ function isRenderableAd(ad) {
58
+ var _a;
59
+ return Boolean(ad && typeof ad === "object" && ((_a = ad.src) == null ? void 0 : _a.trim()));
60
+ }
61
+ function resolveOptions(options) {
62
+ var _a, _b, _c, _d, _e, _f;
63
+ const raw = Array.isArray(options.ads) ? options.ads : [];
64
+ return {
65
+ ads: raw.filter(isRenderableAd),
66
+ // 移动端缺省落在左下角(不挡右侧阅读区/返回顶部按钮)
67
+ position: (_a = options.position) != null ? _a : isMobile() ? "left-bottom" : "left-center",
68
+ closePersistence: (_b = options.closePersistence) != null ? _b : false,
69
+ hideOnMobile: (_c = options.hideOnMobile) != null ? _c : false,
70
+ zIndex: (_d = options.zIndex) != null ? _d : 9999,
71
+ adLabel: options.adLabel === void 0 ? false : options.adLabel,
72
+ imageFit: options.imageFit === "contain" ? "contain" : "cover",
73
+ sizeOverrides: {
74
+ width: Number.isFinite(options.width) ? Math.max(80, Number(options.width)) : void 0,
75
+ offset: Number.isFinite(options.offset) ? Math.max(0, Number(options.offset)) : void 0,
76
+ imageSize: Number.isFinite(options.imageSize) ? Math.max(40, Number(options.imageSize)) : void 0
77
+ },
78
+ theme: { ...DEFAULT_THEME, ...(_e = options.theme) != null ? _e : {} },
79
+ themeOverrides: (_f = options.theme) != null ? _f : {},
80
+ onClick: options.onClick
81
+ };
82
+ }
83
+ var FloatingAd = class {
84
+ constructor(options = {}, container = document.body) {
85
+ this.el = null;
86
+ this.closeBtn = null;
87
+ /** 本次渲染的广告列表(与 DOM 上的 data-index 一一对应) */
88
+ this.rendered = [];
89
+ this.handleClose = () => {
90
+ this.close();
91
+ };
92
+ this.handleImageError = (event) => {
93
+ const img = event.target;
94
+ if (!img || img.tagName !== "IMG" || !img.classList.contains("fa-img")) return;
95
+ const media = img.closest(".fa-media");
96
+ if (!media) return;
97
+ media.classList.add("fa-media--failed");
98
+ const item = media.closest(".fa-item");
99
+ if (item && !item.querySelector(".fa-body")) item.classList.add("fa-item--failed");
100
+ };
101
+ this.handleItemClick = (event) => {
102
+ var _a;
103
+ if (!this.opts.onClick) return;
104
+ const target = event.target;
105
+ const item = (_a = target == null ? void 0 : target.closest) == null ? void 0 : _a.call(target, ".fa-item");
106
+ if (!item) return;
107
+ const index = Number(item.dataset.index);
108
+ const ad = this.rendered[index];
109
+ if (!ad) return;
110
+ this.opts.onClick(ad, event);
111
+ };
112
+ this.opts = resolveOptions(options);
113
+ this.container = container;
114
+ if (this.opts.hideOnMobile && isMobile()) return;
115
+ if (this.opts.closePersistence && hasCloseMark()) return;
116
+ this.render();
117
+ }
118
+ /** 是否已挂载到页面 */
119
+ isMounted() {
120
+ return this.el !== null && this.el.isConnected;
121
+ }
122
+ /** 打开(已关闭后再次显示,忽略 localStorage 关闭标记) */
123
+ open() {
124
+ if (this.isMounted()) return;
125
+ this.render();
126
+ }
127
+ /** 关闭浮窗(完全移除)。默认不记住状态,刷新后重新出现 */
128
+ close() {
129
+ if (this.opts.closePersistence) {
130
+ setCloseMark();
131
+ }
132
+ this.destroy();
133
+ }
134
+ /** 从页面移除并解绑 */
135
+ destroy() {
136
+ var _a, _b, _c, _d;
137
+ (_a = this.closeBtn) == null ? void 0 : _a.removeEventListener("click", this.handleClose);
138
+ this.closeBtn = null;
139
+ (_b = this.el) == null ? void 0 : _b.removeEventListener("click", this.handleItemClick);
140
+ (_c = this.el) == null ? void 0 : _c.removeEventListener("error", this.handleImageError, true);
141
+ (_d = this.el) == null ? void 0 : _d.remove();
142
+ this.el = null;
143
+ this.rendered = [];
144
+ }
145
+ /** 用新配置重渲染(保留原容器) */
146
+ update(options) {
147
+ this.destroy();
148
+ this.opts = resolveOptions(options);
149
+ if (this.opts.hideOnMobile && isMobile()) return;
150
+ if (this.opts.closePersistence && hasCloseMark()) return;
151
+ this.render();
152
+ }
153
+ render() {
154
+ var _a;
155
+ const { ads, position, zIndex, themeOverrides, adLabel, imageFit, sizeOverrides } = this.opts;
156
+ if (ads.length === 0) return;
157
+ const root = document.createElement("div");
158
+ root.className = "fa-widget";
159
+ root.dataset.position = position;
160
+ root.style.zIndex = String(zIndex);
161
+ root.style.setProperty("--fa-img-fit", imageFit);
162
+ if (sizeOverrides.width !== void 0) {
163
+ root.style.setProperty("--fa-width", `${sizeOverrides.width}px`);
164
+ }
165
+ if (sizeOverrides.offset !== void 0) {
166
+ root.style.setProperty("--fa-offset", `${sizeOverrides.offset}px`);
167
+ }
168
+ if (sizeOverrides.imageSize !== void 0) {
169
+ root.style.setProperty("--fa-img-size", `${sizeOverrides.imageSize}px`);
170
+ }
171
+ const overrides = [
172
+ ["--fa-bg", themeOverrides.bg],
173
+ ["--fa-accent", themeOverrides.accent],
174
+ ["--fa-radius", themeOverrides.radius],
175
+ ["--fa-border", themeOverrides.border]
176
+ ];
177
+ for (const [key, value] of overrides) {
178
+ if (value !== void 0) root.style.setProperty(key, value);
179
+ }
180
+ const label = adLabel ? escapeHtml(String(adLabel)) : "";
181
+ root.innerHTML = `
182
+ <button class="fa-close" type="button" aria-label="\u5173\u95ED\u5E7F\u544A">${CLOSE_SVG}</button>
183
+ ${ads.map((ad, index) => this.renderItem(ad, index, index === 0 ? label : "")).join('<div class="fa-divider" role="separator"></div>')}
184
+ `;
185
+ this.closeBtn = root.querySelector(".fa-close");
186
+ (_a = this.closeBtn) == null ? void 0 : _a.addEventListener("click", this.handleClose);
187
+ root.addEventListener("click", this.handleItemClick);
188
+ root.addEventListener("error", this.handleImageError, true);
189
+ this.rendered = ads;
190
+ this.container.appendChild(root);
191
+ this.el = root;
192
+ }
193
+ renderItem(ad, index, label) {
194
+ var _a, _b;
195
+ const media = `<span class="fa-media">
196
+ <img class="fa-img" src="${escapeAttr(ad.src)}" alt="${escapeAttr((_b = (_a = ad.alt) != null ? _a : ad.title) != null ? _b : "")}" loading="lazy" decoding="async" />
197
+ ${ad.badge ? `<span class="fa-tag">${escapeHtml(ad.badge)}</span>` : ""}
198
+ ${label ? `<span class="fa-label">${label}</span>` : ""}
199
+ </span>`;
200
+ const body = ad.title || ad.desc ? `<span class="fa-body">
201
+ ${ad.title ? `<span class="fa-title">${escapeHtml(ad.title)}</span>` : ""}
202
+ ${ad.desc ? `<span class="fa-desc">${escapeHtml(ad.desc)}</span>` : ""}
203
+ </span>` : "";
204
+ const inner = `${media}${body}`;
205
+ const href = ad.href ? safeHref(ad.href) : null;
206
+ if (href) {
207
+ const target = ad.target === "_self" ? "_self" : "_blank";
208
+ const rel = target === "_blank" ? ' rel="noopener noreferrer nofollow sponsored"' : ' rel="nofollow sponsored"';
209
+ return `<a class="fa-item" href="${escapeAttr(href)}" target="${target}"${rel} data-index="${index}">${inner}</a>`;
210
+ }
211
+ return `<div class="fa-item" data-index="${index}">${inner}</div>`;
212
+ }
213
+ };
214
+ 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>';
215
+ function safeHref(href) {
216
+ const trimmed = href.trim();
217
+ if (!trimmed) return null;
218
+ const compact = trimmed.replace(/[\s\u0000-\u001f]/g, "").toLowerCase();
219
+ if (/^(javascript|data|vbscript|file):/.test(compact)) return null;
220
+ return trimmed;
221
+ }
222
+ function escapeHtml(value) {
223
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
224
+ }
225
+ function escapeAttr(value) {
226
+ return escapeHtml(value);
227
+ }
228
+
229
+ // src/index.ts
230
+ var index_default = FloatingAd;
231
+ // Annotate the CommonJS export names for ESM import in node:
232
+ 0 && (module.exports = {
233
+ FloatingAd
234
+ });
235
+ //# sourceMappingURL=index.umd.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/FloatingAd.ts"],"sourcesContent":["import { FloatingAd } from './FloatingAd'\n\nexport { FloatingAd }\nexport default FloatingAd\nexport type { FloatingAdOptions, FloatingAdItem, FloatingAdTheme, Position } from './FloatingAd'\n","export type Position =\n | 'right-bottom'\n | 'right-top'\n | 'right-center'\n | 'left-bottom'\n | 'left-top'\n | 'left-center'\n\n/** 单条广告 */\nexport interface FloatingAdItem {\n /** 广告图片地址(必填;没有图片的条目不会渲染) */\n src: string\n /** 标题 */\n title?: string\n /** 副文案 / 卖点 */\n desc?: string\n /** 点击跳转链接(可选,不传则不可点击) */\n href?: string\n /** 跳转方式,默认 `_blank`(仅在传了 href 时生效) */\n target?: '_blank' | '_self'\n /** 图片 alt,缺省使用标题 */\n alt?: string\n /** 图片右上角角标文案,如「限时」 */\n badge?: string\n}\n\nexport interface FloatingAdTheme {\n /** 浮窗背景色 */\n bg?: string\n /** 强调色(标题、角标) */\n accent?: string\n /** 圆角大小 */\n radius?: string\n /** 边框颜色 */\n border?: string\n}\n\nexport interface FloatingAdOptions {\n /** 广告列表(按数组顺序自上而下排列;为空时不渲染浮窗) */\n ads?: FloatingAdItem[]\n /** 浮窗位置,缺省:桌面 left-center / 移动端(<768px)left-bottom */\n position?: Position\n /** 关闭后是否记住状态(写入 localStorage),默认 false —— false 时刷新页面必然重新出现 */\n closePersistence?: boolean\n /** 移动端(<768px)是否隐藏,默认 false(移动端展示更小一号的浮窗) */\n hideOnMobile?: boolean\n /** 浮窗 z-index,默认 9999 */\n zIndex?: number\n /** 「广告」标识文案(压在首图左上角),默认 false 不显示;传字符串则显示该文案 */\n adLabel?: string | false\n /** 浮窗宽度(px),缺省:桌面 150 / 移动端 140 */\n width?: number\n /** 浮窗距视口边缘距离(px),缺省:桌面 16 / 移动端 12 */\n offset?: number\n /** 图片填充方式,默认 cover */\n imageFit?: 'cover' | 'contain'\n /** 广告图片边长(px,正方形),缺省:桌面 120 / 移动端 112 */\n imageSize?: number\n /** 主题(映射为 CSS 变量,也可直接覆盖 --fa-* 变量) */\n theme?: FloatingAdTheme\n /** 点击广告回调(不影响默认跳转行为) */\n onClick?: (ad: FloatingAdItem, event: MouseEvent) => void\n}\n\ninterface ResolvedOptions {\n ads: FloatingAdItem[]\n position: Position\n closePersistence: boolean\n hideOnMobile: boolean\n zIndex: number\n adLabel: string | false\n imageFit: 'cover' | 'contain'\n /** 用户显式传入的尺寸子集(未配置时不写 inline,交给 CSS 按桌面/移动端默认值走) */\n sizeOverrides: {\n width?: number\n offset?: number\n imageSize?: number\n }\n theme: Required<FloatingAdTheme>\n /** 用户显式传入的主题子集(未配置的 key 交给 CSS 默认值,可随系统深浅色变化) */\n themeOverrides: FloatingAdTheme\n onClick?: (ad: FloatingAdItem, event: MouseEvent) => void\n}\n\nconst DEFAULT_THEME: Required<FloatingAdTheme> = {\n bg: 'rgba(255, 255, 255, 0.96)',\n accent: '#333',\n radius: '12px',\n border: 'rgba(0, 0, 0, 0.1)'\n}\n\nconst STORAGE_KEY = 'floating-ad:closed'\nconst MOBILE_QUERY = '(max-width: 767px)'\n\nfunction isMobile(): boolean {\n if (typeof window === 'undefined') return false\n return window.matchMedia?.(MOBILE_QUERY)?.matches ?? false\n}\n\nfunction hasCloseMark(): boolean {\n if (typeof localStorage === 'undefined') return false\n try {\n return localStorage.getItem(STORAGE_KEY) === '1'\n } catch {\n return false\n }\n}\n\nfunction setCloseMark(): void {\n if (typeof localStorage === 'undefined') return\n try {\n localStorage.setItem(STORAGE_KEY, '1')\n } catch {\n /* ignore quota / privacy mode */\n }\n}\n\n/** 只有带图片的条目才会渲染(本组件只承载图片广告) */\nfunction isRenderableAd(ad: FloatingAdItem | null | undefined): ad is FloatingAdItem {\n return Boolean(ad && typeof ad === 'object' && ad.src?.trim())\n}\n\nfunction resolveOptions(options: FloatingAdOptions): ResolvedOptions {\n const raw = Array.isArray(options.ads) ? options.ads : []\n return {\n ads: raw.filter(isRenderableAd),\n // 移动端缺省落在左下角(不挡右侧阅读区/返回顶部按钮)\n position: options.position ?? (isMobile() ? 'left-bottom' : 'left-center'),\n closePersistence: options.closePersistence ?? false,\n hideOnMobile: options.hideOnMobile ?? false,\n zIndex: options.zIndex ?? 9999,\n adLabel: options.adLabel === undefined ? false : options.adLabel,\n imageFit: options.imageFit === 'contain' ? 'contain' : 'cover',\n sizeOverrides: {\n width: Number.isFinite(options.width) ? Math.max(80, Number(options.width)) : undefined,\n offset: Number.isFinite(options.offset) ? Math.max(0, Number(options.offset)) : undefined,\n imageSize: Number.isFinite(options.imageSize)\n ? Math.max(40, Number(options.imageSize))\n : undefined\n },\n theme: { ...DEFAULT_THEME, ...(options.theme ?? {}) },\n themeOverrides: options.theme ?? {},\n onClick: options.onClick\n }\n}\n\n/**\n * A minimal floating ad slot that pins image / text ads to the left (or right) edge.\n * Zero dependencies. Framework-agnostic. Styleable via --fa-* CSS variables.\n *\n * 没有广告数据时不渲染任何节点(不会留下空白卡片)。\n *\n * @param container 渲染容器,默认 document.body;Web Component 场景传入 shadow root\n */\nexport class FloatingAd {\n private opts: ResolvedOptions\n private readonly container: HTMLElement | ShadowRoot\n private el: HTMLElement | null = null\n private closeBtn: HTMLButtonElement | null = null\n /** 本次渲染的广告列表(与 DOM 上的 data-index 一一对应) */\n private rendered: FloatingAdItem[] = []\n\n constructor(\n options: FloatingAdOptions = {},\n container: HTMLElement | ShadowRoot = document.body\n ) {\n this.opts = resolveOptions(options)\n this.container = container\n\n if (this.opts.hideOnMobile && isMobile()) return\n if (this.opts.closePersistence && hasCloseMark()) return\n\n this.render()\n }\n\n /** 是否已挂载到页面 */\n isMounted(): boolean {\n return this.el !== null && this.el.isConnected\n }\n\n /** 打开(已关闭后再次显示,忽略 localStorage 关闭标记) */\n open(): void {\n if (this.isMounted()) return\n this.render()\n }\n\n /** 关闭浮窗(完全移除)。默认不记住状态,刷新后重新出现 */\n close(): void {\n if (this.opts.closePersistence) {\n setCloseMark()\n }\n this.destroy()\n }\n\n /** 从页面移除并解绑 */\n destroy(): void {\n this.closeBtn?.removeEventListener('click', this.handleClose)\n this.closeBtn = null\n this.el?.removeEventListener('click', this.handleItemClick)\n this.el?.removeEventListener('error', this.handleImageError, true)\n this.el?.remove()\n this.el = null\n this.rendered = []\n }\n\n /** 用新配置重渲染(保留原容器) */\n update(options: FloatingAdOptions): void {\n this.destroy()\n this.opts = resolveOptions(options)\n\n if (this.opts.hideOnMobile && isMobile()) return\n if (this.opts.closePersistence && hasCloseMark()) return\n\n this.render()\n }\n\n private render(): void {\n const { ads, position, zIndex, themeOverrides, adLabel, imageFit, sizeOverrides } = this.opts\n\n if (ads.length === 0) return\n\n const root = document.createElement('div')\n root.className = 'fa-widget'\n root.dataset.position = position\n root.style.zIndex = String(zIndex)\n root.style.setProperty('--fa-img-fit', imageFit)\n // 尺寸只在用户显式配置时写 inline,其余交给 CSS(桌面 / 移动端各一套默认值)\n if (sizeOverrides.width !== undefined) {\n root.style.setProperty('--fa-width', `${sizeOverrides.width}px`)\n }\n if (sizeOverrides.offset !== undefined) {\n root.style.setProperty('--fa-offset', `${sizeOverrides.offset}px`)\n }\n if (sizeOverrides.imageSize !== undefined) {\n root.style.setProperty('--fa-img-size', `${sizeOverrides.imageSize}px`)\n }\n\n // 仅把用户显式设置的主题写为 inline(用户优先);\n // 未设置的 key 交给 CSS 变量默认值,以便跟随系统深浅色(prefers-color-scheme)\n const overrides = [\n ['--fa-bg', themeOverrides.bg],\n ['--fa-accent', themeOverrides.accent],\n ['--fa-radius', themeOverrides.radius],\n ['--fa-border', themeOverrides.border]\n ] as const\n for (const [key, value] of overrides) {\n if (value !== undefined) root.style.setProperty(key, value)\n }\n\n // 「广告」标识不单独占行:压在首图左上角\n const label = adLabel ? escapeHtml(String(adLabel)) : ''\n\n root.innerHTML = `\n <button class=\"fa-close\" type=\"button\" aria-label=\"关闭广告\">${CLOSE_SVG}</button>\n ${ads\n .map((ad, index) => this.renderItem(ad, index, index === 0 ? label : ''))\n .join('<div class=\"fa-divider\" role=\"separator\"></div>')}\n `\n\n this.closeBtn = root.querySelector<HTMLButtonElement>('.fa-close')\n this.closeBtn?.addEventListener('click', this.handleClose)\n root.addEventListener('click', this.handleItemClick)\n // 图片加载失败时降级:隐藏空图框,仅保留文字;连文字都没有就整条隐藏\n root.addEventListener('error', this.handleImageError, true)\n\n this.rendered = ads\n this.container.appendChild(root)\n this.el = root\n }\n\n private renderItem(ad: FloatingAdItem, index: number, label: string): string {\n const media = `<span class=\"fa-media\">\n <img class=\"fa-img\" src=\"${escapeAttr(ad.src)}\" alt=\"${escapeAttr(ad.alt ?? ad.title ?? '')}\" loading=\"lazy\" decoding=\"async\" />\n ${ad.badge ? `<span class=\"fa-tag\">${escapeHtml(ad.badge)}</span>` : ''}\n ${label ? `<span class=\"fa-label\">${label}</span>` : ''}\n </span>`\n\n const body =\n ad.title || ad.desc\n ? `<span class=\"fa-body\">\n ${ad.title ? `<span class=\"fa-title\">${escapeHtml(ad.title)}</span>` : ''}\n ${ad.desc ? `<span class=\"fa-desc\">${escapeHtml(ad.desc)}</span>` : ''}\n </span>`\n : ''\n\n const inner = `${media}${body}`\n\n const href = ad.href ? safeHref(ad.href) : null\n if (href) {\n const target = ad.target === '_self' ? '_self' : '_blank'\n const rel =\n target === '_blank'\n ? ' rel=\"noopener noreferrer nofollow sponsored\"'\n : ' rel=\"nofollow sponsored\"'\n return `<a class=\"fa-item\" href=\"${escapeAttr(href)}\" target=\"${target}\"${rel} data-index=\"${index}\">${inner}</a>`\n }\n\n return `<div class=\"fa-item\" data-index=\"${index}\">${inner}</div>`\n }\n\n private readonly handleClose = (): void => {\n this.close()\n }\n\n private readonly handleImageError = (event: Event): void => {\n const img = event.target as HTMLElement | null\n if (!img || img.tagName !== 'IMG' || !img.classList.contains('fa-img')) return\n const media = img.closest('.fa-media')\n if (!media) return\n media.classList.add('fa-media--failed')\n const item = media.closest('.fa-item')\n if (item && !item.querySelector('.fa-body')) item.classList.add('fa-item--failed')\n }\n\n private readonly handleItemClick = (event: Event): void => {\n if (!this.opts.onClick) return\n const target = event.target as Element | null\n const item = target?.closest?.('.fa-item') as HTMLElement | null\n if (!item) return\n const index = Number(item.dataset.index)\n const ad = this.rendered[index]\n if (!ad) return\n this.opts.onClick(ad, event as MouseEvent)\n }\n}\n\nconst CLOSE_SVG =\n '<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>'\n\n/** 过滤危险协议(javascript: / data: / vbscript: 等),返回可用链接或 null */\nfunction safeHref(href: string): string | null {\n const trimmed = href.trim()\n if (!trimmed) return null\n const compact = trimmed.replace(/[\\s\\u0000-\\u001f]/g, '').toLowerCase()\n if (/^(javascript|data|vbscript|file):/.test(compact)) return null\n return trimmed\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\n}\n\nfunction escapeAttr(value: string): string {\n return escapeHtml(value)\n}\n"],"mappings":"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","names":[]}