universal-ad-loader 0.1.2 → 0.2.0
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.
- package/CHANGELOG.md +19 -0
- package/README.md +37 -5
- package/dist/index.cjs +363 -46
- package/dist/index.d.ts +46 -4
- package/dist/index.mjs +363 -46
- package/dist/ual.js +363 -46
- package/dist/ual.min.js +24 -5
- package/docs/cdn.md +30 -15
- package/docs/esempio.md +1 -1
- package/package.json +3 -3
- package/schema/ual-config.schema.json +9 -1
package/dist/index.d.ts
CHANGED
|
@@ -143,6 +143,11 @@ export interface SlotConfig<P extends string = string> {
|
|
|
143
143
|
maxRefresh?: number;
|
|
144
144
|
enabled?: boolean;
|
|
145
145
|
viewability?: ViewabilityConfig;
|
|
146
|
+
/**
|
|
147
|
+
* Banner ancorato al bordo dello schermo con pulsante di chiusura. Solo se l'annuncio occupa al massimo
|
|
148
|
+
* il 30% dell'altezza dello schermo (Better Ads); altrimenti resta nel flusso della pagina.
|
|
149
|
+
*/
|
|
150
|
+
sticky?: boolean | 'top' | 'bottom' | { position?: 'top' | 'bottom' };
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
export interface PageRule {
|
|
@@ -208,6 +213,8 @@ export interface UalEventMap {
|
|
|
208
213
|
slotEmpty: SlotInfo;
|
|
209
214
|
slotViewable: SlotInfo;
|
|
210
215
|
slotClick: SlotInfo;
|
|
216
|
+
/** Video: `start`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`. */
|
|
217
|
+
videoEvent: SlotInfo & { event: 'start' | 'firstQuartile' | 'midpoint' | 'thirdQuartile' | 'complete' };
|
|
211
218
|
error: { provider?: string; slot?: string; error: Error };
|
|
212
219
|
}
|
|
213
220
|
|
|
@@ -230,15 +237,50 @@ export interface AdapterContext {
|
|
|
230
237
|
debug: boolean;
|
|
231
238
|
}
|
|
232
239
|
|
|
240
|
+
/** Asset di una creatività `native`: sono dati, mai HTML. UAL li disegna con l'etichetta "Sponsored". */
|
|
241
|
+
export interface NativeAssets {
|
|
242
|
+
/** Obbligatorio, massimo 150 caratteri (il resto viene troncato). */
|
|
243
|
+
title: string;
|
|
244
|
+
body?: string;
|
|
245
|
+
image?: string | { src: string; width?: number; height?: number };
|
|
246
|
+
icon?: string | { src: string; width?: number; height?: number };
|
|
247
|
+
sponsoredBy?: string;
|
|
248
|
+
cta?: string;
|
|
249
|
+
displayUrl?: string;
|
|
250
|
+
price?: string;
|
|
251
|
+
/** 0-5 */
|
|
252
|
+
rating?: number;
|
|
253
|
+
layout?: 'card' | 'list' | 'compact';
|
|
254
|
+
/** Testo dell'etichetta (default "Sponsored"); l'etichetta c'è sempre. */
|
|
255
|
+
label?: string;
|
|
256
|
+
}
|
|
257
|
+
|
|
233
258
|
export interface AdResult {
|
|
234
|
-
type?: 'image' | 'html' | 'url';
|
|
259
|
+
type?: 'image' | 'html' | 'url' | 'native' | 'video';
|
|
260
|
+
/** image: file immagine. video: file .mp4/.webm. */
|
|
235
261
|
src?: string;
|
|
236
262
|
html?: string;
|
|
237
263
|
url?: string;
|
|
238
|
-
|
|
239
|
-
|
|
264
|
+
native?: NativeAssets;
|
|
265
|
+
/** video */
|
|
266
|
+
poster?: string;
|
|
267
|
+
/** video, es. `video/mp4` */
|
|
268
|
+
mime?: string;
|
|
269
|
+
/** Obbligatori, tranne per `native` (segue la larghezza della colonna). */
|
|
270
|
+
width?: number;
|
|
271
|
+
height?: number;
|
|
240
272
|
clickUrl?: string;
|
|
241
|
-
trackers?: {
|
|
273
|
+
trackers?: {
|
|
274
|
+
impression?: string[];
|
|
275
|
+
viewable?: string[];
|
|
276
|
+
click?: string[];
|
|
277
|
+
/** video: alla prima riproduzione */
|
|
278
|
+
start?: string[];
|
|
279
|
+
/** video */
|
|
280
|
+
quartile?: { firstQuartile?: string[]; midpoint?: string[]; thirdQuartile?: string[]; complete?: string[] };
|
|
281
|
+
/** Partono quando la creatività HTML chiama `UALExt.track(nome)`. */
|
|
282
|
+
custom?: Record<string, string[]>;
|
|
283
|
+
};
|
|
242
284
|
sandbox?: string;
|
|
243
285
|
}
|
|
244
286
|
|
package/dist/index.mjs
CHANGED
|
@@ -271,6 +271,247 @@ function normalize(tc, opts) {
|
|
|
271
271
|
return out;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
// src/core/native.js
|
|
275
|
+
var NATIVE_LIMITS = { title: 150, body: 300, sponsoredBy: 60, cta: 30, displayUrl: 100, price: 30, label: 20 };
|
|
276
|
+
var NATIVE_LAYOUTS = ["card", "list", "compact"];
|
|
277
|
+
var clip = (v, n) => typeof v === "string" ? v.trim().slice(0, n) : "";
|
|
278
|
+
function normalizeNative(raw, safeUrl2) {
|
|
279
|
+
if (!raw || typeof raw !== "object") return { ok: false, reason: "native mancante" };
|
|
280
|
+
const title = clip(raw.title, NATIVE_LIMITS.title);
|
|
281
|
+
if (!title) return { ok: false, reason: "native: titolo mancante" };
|
|
282
|
+
const img = (v) => {
|
|
283
|
+
const o = typeof v === "string" ? { src: v } : v;
|
|
284
|
+
const src = o && typeof o === "object" ? safeUrl2(o.src, true) : null;
|
|
285
|
+
if (!src) return null;
|
|
286
|
+
const dim2 = (n) => {
|
|
287
|
+
const x = Math.round(Number(n));
|
|
288
|
+
return x > 0 && x <= 2e3 ? x : 0;
|
|
289
|
+
};
|
|
290
|
+
return { src, width: dim2(o.width), height: dim2(o.height) };
|
|
291
|
+
};
|
|
292
|
+
const rating = Number(raw.rating);
|
|
293
|
+
return {
|
|
294
|
+
ok: true,
|
|
295
|
+
native: {
|
|
296
|
+
title,
|
|
297
|
+
body: clip(raw.body, NATIVE_LIMITS.body),
|
|
298
|
+
sponsoredBy: clip(raw.sponsoredBy, NATIVE_LIMITS.sponsoredBy),
|
|
299
|
+
cta: clip(raw.cta, NATIVE_LIMITS.cta),
|
|
300
|
+
displayUrl: clip(raw.displayUrl, NATIVE_LIMITS.displayUrl),
|
|
301
|
+
price: clip(raw.price, NATIVE_LIMITS.price),
|
|
302
|
+
rating: Number.isFinite(rating) && rating >= 0 && rating <= 5 ? rating : null,
|
|
303
|
+
image: img(raw.image),
|
|
304
|
+
icon: img(raw.icon),
|
|
305
|
+
layout: NATIVE_LAYOUTS.includes(raw.layout) ? raw.layout : "card",
|
|
306
|
+
label: clip(raw.label, NATIVE_LIMITS.label) || "Sponsored"
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
var STYLE = `
|
|
311
|
+
:host{display:block;max-width:100%;font:inherit;color:inherit;line-height:1.35}
|
|
312
|
+
*{box-sizing:border-box}
|
|
313
|
+
.card{display:flex;flex-direction:column;gap:.5em;padding:.75em;text-decoration:none;color:inherit;border:1px solid var(--ual-native-border,rgba(128,128,128,.35));border-radius:var(--ual-native-radius,8px);background:var(--ual-native-bg,transparent);position:relative}
|
|
314
|
+
a.card:hover .title{text-decoration:underline}
|
|
315
|
+
.label{font-size:.72em;letter-spacing:.04em;text-transform:uppercase;opacity:.75}
|
|
316
|
+
.main{display:block;width:100%;height:auto;border-radius:calc(var(--ual-native-radius,8px) - 2px);background:rgba(128,128,128,.12)}
|
|
317
|
+
.row{display:flex;gap:.6em;align-items:flex-start}
|
|
318
|
+
.icon{width:2.4em;height:2.4em;flex:none;border-radius:6px;object-fit:cover}
|
|
319
|
+
.title{font-weight:700;font-size:1.05em}
|
|
320
|
+
.body{opacity:.9;font-size:.92em}
|
|
321
|
+
.meta{font-size:.8em;opacity:.7}
|
|
322
|
+
.cta{align-self:flex-start;font-weight:600;font-size:.9em;padding:.35em .8em;border-radius:999px;background:var(--ual-native-accent,#2563eb);color:var(--ual-native-accent-text,#fff)}
|
|
323
|
+
.price{font-weight:600}
|
|
324
|
+
.layout-list{flex-direction:row;align-items:flex-start}
|
|
325
|
+
.layout-list .main{width:35%;max-width:180px;flex:none}
|
|
326
|
+
.layout-list .text{display:flex;flex-direction:column;gap:.35em;min-width:0}
|
|
327
|
+
.layout-compact .main{display:none}
|
|
328
|
+
`;
|
|
329
|
+
function el(tag, cls, text) {
|
|
330
|
+
const e = document.createElement(tag);
|
|
331
|
+
if (cls) e.className = cls;
|
|
332
|
+
if (text != null) e.textContent = text;
|
|
333
|
+
return e;
|
|
334
|
+
}
|
|
335
|
+
function image(i, cls, alt) {
|
|
336
|
+
const im = el("img", cls);
|
|
337
|
+
im.src = i.src;
|
|
338
|
+
im.alt = alt || "";
|
|
339
|
+
im.loading = "lazy";
|
|
340
|
+
im.referrerPolicy = "strict-origin-when-cross-origin";
|
|
341
|
+
if (i.width && i.height) {
|
|
342
|
+
im.width = i.width;
|
|
343
|
+
im.height = i.height;
|
|
344
|
+
}
|
|
345
|
+
return im;
|
|
346
|
+
}
|
|
347
|
+
function renderNative(container, ad, { onClick } = {}) {
|
|
348
|
+
const n = ad.native;
|
|
349
|
+
const host = el("div", "ual-native");
|
|
350
|
+
const root = host.attachShadow ? host.attachShadow({ mode: "open" }) : host;
|
|
351
|
+
const style = document.createElement("style");
|
|
352
|
+
style.textContent = STYLE;
|
|
353
|
+
root.appendChild(style);
|
|
354
|
+
const card = ad.clickUrl ? el("a", `card layout-${n.layout}`) : el("div", `card layout-${n.layout}`);
|
|
355
|
+
if (ad.clickUrl) {
|
|
356
|
+
card.href = ad.clickUrl;
|
|
357
|
+
card.target = "_blank";
|
|
358
|
+
card.rel = "noopener noreferrer sponsored nofollow";
|
|
359
|
+
card.addEventListener("click", () => {
|
|
360
|
+
if (onClick) onClick();
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
card.appendChild(el("span", "label", n.label));
|
|
364
|
+
if (n.image) card.appendChild(image(n.image, "main", n.title));
|
|
365
|
+
const text = el("div", "text");
|
|
366
|
+
const head = el("div", "row");
|
|
367
|
+
if (n.icon) head.appendChild(image(n.icon, "icon", ""));
|
|
368
|
+
const titles = el("div");
|
|
369
|
+
titles.appendChild(el("div", "title", n.title));
|
|
370
|
+
if (n.sponsoredBy) titles.appendChild(el("div", "meta", n.sponsoredBy));
|
|
371
|
+
head.appendChild(titles);
|
|
372
|
+
text.appendChild(head);
|
|
373
|
+
if (n.body) text.appendChild(el("div", "body", n.body));
|
|
374
|
+
const meta = [n.displayUrl, n.rating !== null ? `\u2605 ${n.rating}` : ""].filter(Boolean).join(" \xB7 ");
|
|
375
|
+
if (meta) text.appendChild(el("div", "meta", meta));
|
|
376
|
+
if (n.price) text.appendChild(el("div", "price", n.price));
|
|
377
|
+
if (n.cta) text.appendChild(el("span", "cta", n.cta));
|
|
378
|
+
card.appendChild(text);
|
|
379
|
+
root.appendChild(card);
|
|
380
|
+
container.innerHTML = "";
|
|
381
|
+
container.appendChild(host);
|
|
382
|
+
return { el: host, destroy: () => host.remove() };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/core/video.js
|
|
386
|
+
var QUARTILES = [["firstQuartile", 0.25], ["midpoint", 0.5], ["thirdQuartile", 0.75]];
|
|
387
|
+
var QUARTILE_NAMES = ["firstQuartile", "midpoint", "thirdQuartile", "complete"];
|
|
388
|
+
var PLAY_THRESHOLD = 0.5;
|
|
389
|
+
function quartileHits(fired, currentTime, duration) {
|
|
390
|
+
if (!(duration > 0) || !(currentTime >= 0)) return [];
|
|
391
|
+
const f = currentTime / duration;
|
|
392
|
+
const hits = [];
|
|
393
|
+
for (const [name, at] of QUARTILES) {
|
|
394
|
+
if (f >= at && !fired.has(name)) {
|
|
395
|
+
fired.add(name);
|
|
396
|
+
hits.push(name);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return hits;
|
|
400
|
+
}
|
|
401
|
+
function normalizeVideo(raw, safeUrl2) {
|
|
402
|
+
const src = safeUrl2(raw.src);
|
|
403
|
+
if (!src) return { ok: false, reason: "video: src non valido (ammessi solo http e https)" };
|
|
404
|
+
return { ok: true, video: { src, poster: safeUrl2(raw.poster, true) || "", mime: typeof raw.mime === "string" && /^video\/[\w.+-]+$/.test(raw.mime) ? raw.mime : "" } };
|
|
405
|
+
}
|
|
406
|
+
function btn(label) {
|
|
407
|
+
const b = document.createElement("button");
|
|
408
|
+
b.type = "button";
|
|
409
|
+
b.textContent = label;
|
|
410
|
+
b.style.cssText = "position:absolute;right:8px;bottom:8px;z-index:2;padding:4px 10px;border:0;border-radius:999px;background:rgba(0,0,0,.65);color:#fff;font:600 12px/1.4 system-ui,sans-serif;cursor:pointer";
|
|
411
|
+
return b;
|
|
412
|
+
}
|
|
413
|
+
function renderVideo(container, ad, { onClick, onEvent, onError } = {}) {
|
|
414
|
+
const wrap = document.createElement("div");
|
|
415
|
+
wrap.className = "ual-video";
|
|
416
|
+
wrap.style.cssText = `position:relative;display:block;margin:0 auto;width:${ad.width}px;max-width:100%;aspect-ratio:${ad.width}/${ad.height};background:#000;overflow:hidden`;
|
|
417
|
+
const v = document.createElement("video");
|
|
418
|
+
v.muted = true;
|
|
419
|
+
v.defaultMuted = true;
|
|
420
|
+
v.playsInline = true;
|
|
421
|
+
v.setAttribute("playsinline", "");
|
|
422
|
+
v.preload = "metadata";
|
|
423
|
+
v.disablePictureInPicture = true;
|
|
424
|
+
v.controls = false;
|
|
425
|
+
v.style.cssText = "display:block;width:100%;height:100%;object-fit:contain";
|
|
426
|
+
if (ad.video.poster) v.poster = ad.video.poster;
|
|
427
|
+
const source = document.createElement("source");
|
|
428
|
+
source.src = ad.video.src;
|
|
429
|
+
if (ad.video.mime) source.type = ad.video.mime;
|
|
430
|
+
v.appendChild(source);
|
|
431
|
+
wrap.appendChild(v);
|
|
432
|
+
const label = document.createElement("span");
|
|
433
|
+
label.textContent = "Advertisement";
|
|
434
|
+
label.style.cssText = "position:absolute;left:8px;top:8px;z-index:2;padding:2px 6px;border-radius:4px;background:rgba(0,0,0,.55);color:#fff;font:600 11px/1.4 system-ui,sans-serif";
|
|
435
|
+
wrap.appendChild(label);
|
|
436
|
+
if (ad.clickUrl) {
|
|
437
|
+
const a = document.createElement("a");
|
|
438
|
+
a.href = ad.clickUrl;
|
|
439
|
+
a.target = "_blank";
|
|
440
|
+
a.rel = "noopener noreferrer sponsored nofollow";
|
|
441
|
+
a.setAttribute("aria-label", "Open the advertiser page");
|
|
442
|
+
a.style.cssText = "position:absolute;inset:0;z-index:1";
|
|
443
|
+
a.addEventListener("click", () => {
|
|
444
|
+
if (onClick) onClick();
|
|
445
|
+
});
|
|
446
|
+
wrap.appendChild(a);
|
|
447
|
+
}
|
|
448
|
+
const sound = btn("Sound on");
|
|
449
|
+
sound.setAttribute("aria-label", "Turn sound on");
|
|
450
|
+
sound.addEventListener("click", () => {
|
|
451
|
+
v.muted = !v.muted;
|
|
452
|
+
sound.textContent = v.muted ? "Sound on" : "Sound off";
|
|
453
|
+
sound.setAttribute("aria-label", v.muted ? "Turn sound on" : "Turn sound off");
|
|
454
|
+
});
|
|
455
|
+
wrap.appendChild(sound);
|
|
456
|
+
const trackers = ad.trackers || {};
|
|
457
|
+
const fired = /* @__PURE__ */ new Set();
|
|
458
|
+
let started = false;
|
|
459
|
+
const emit = (name, urls) => {
|
|
460
|
+
fire(urls);
|
|
461
|
+
if (onEvent) onEvent(name);
|
|
462
|
+
};
|
|
463
|
+
v.addEventListener("playing", () => {
|
|
464
|
+
if (!started) {
|
|
465
|
+
started = true;
|
|
466
|
+
emit("start", trackers.start);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
v.addEventListener("timeupdate", () => {
|
|
470
|
+
quartileHits(fired, v.currentTime, v.duration).forEach((n) => emit(n, trackers.quartile && trackers.quartile[n]));
|
|
471
|
+
});
|
|
472
|
+
v.addEventListener("ended", () => {
|
|
473
|
+
quartileHits(fired, v.duration, v.duration).forEach((n) => emit(n, trackers.quartile && trackers.quartile[n]));
|
|
474
|
+
if (!fired.has("complete")) {
|
|
475
|
+
fired.add("complete");
|
|
476
|
+
emit("complete", trackers.quartile && trackers.quartile.complete);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
v.addEventListener("error", () => {
|
|
480
|
+
if (onError) onError(new Error("video: file non riproducibile"));
|
|
481
|
+
});
|
|
482
|
+
let inView = false;
|
|
483
|
+
const sync = () => {
|
|
484
|
+
if (v.ended) return;
|
|
485
|
+
if (inView && document.visibilityState === "visible") {
|
|
486
|
+
const p = v.play();
|
|
487
|
+
if (p && p.catch) p.catch(() => {
|
|
488
|
+
});
|
|
489
|
+
} else v.pause();
|
|
490
|
+
};
|
|
491
|
+
const io = "IntersectionObserver" in window ? new IntersectionObserver((entries) => {
|
|
492
|
+
entries.forEach((en) => {
|
|
493
|
+
inView = en.intersectionRatio >= PLAY_THRESHOLD;
|
|
494
|
+
});
|
|
495
|
+
sync();
|
|
496
|
+
}, { threshold: [0, PLAY_THRESHOLD, 1] }) : null;
|
|
497
|
+
if (io) io.observe(wrap);
|
|
498
|
+
document.addEventListener("visibilitychange", sync);
|
|
499
|
+
container.innerHTML = "";
|
|
500
|
+
container.appendChild(wrap);
|
|
501
|
+
return {
|
|
502
|
+
el: wrap,
|
|
503
|
+
destroy: () => {
|
|
504
|
+
if (io) io.disconnect();
|
|
505
|
+
document.removeEventListener("visibilitychange", sync);
|
|
506
|
+
v.pause();
|
|
507
|
+
v.removeAttribute("src");
|
|
508
|
+
source.remove();
|
|
509
|
+
v.load();
|
|
510
|
+
wrap.remove();
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
274
515
|
// src/core/render.js
|
|
275
516
|
var DEFAULT_SANDBOX = "allow-scripts allow-popups allow-popups-to-escape-sandbox";
|
|
276
517
|
var MAX_DIM = 2e3;
|
|
@@ -293,12 +534,20 @@ function safeUrl(u, allowDataImage) {
|
|
|
293
534
|
function cleanList(list) {
|
|
294
535
|
return (Array.isArray(list) ? list : []).map((u) => safeUrl(u)).filter(Boolean);
|
|
295
536
|
}
|
|
537
|
+
function cleanCustom(custom) {
|
|
538
|
+
const out = {};
|
|
539
|
+
if (!custom || typeof custom !== "object") return out;
|
|
540
|
+
Object.keys(custom).slice(0, 20).forEach((name) => {
|
|
541
|
+
if (/^[\w-]{1,40}$/.test(name)) out[name] = cleanList(custom[name]);
|
|
542
|
+
});
|
|
543
|
+
return out;
|
|
544
|
+
}
|
|
296
545
|
function normalizeAd(raw) {
|
|
297
546
|
if (!raw || typeof raw !== "object") return { ok: false, reason: "annuncio non valido" };
|
|
298
547
|
const type = raw.type || (raw.url ? "url" : raw.src ? "image" : "html");
|
|
299
548
|
const width = dim(raw.width);
|
|
300
549
|
const height = dim(raw.height);
|
|
301
|
-
if (!width || !height) return { ok: false, reason: `dimensioni non valide (${raw.width}x${raw.height}, max ${MAX_DIM})` };
|
|
550
|
+
if (type !== "native" && (!width || !height)) return { ok: false, reason: `dimensioni non valide (${raw.width}x${raw.height}, max ${MAX_DIM})` };
|
|
302
551
|
const t = raw.trackers || {};
|
|
303
552
|
const ad = {
|
|
304
553
|
type,
|
|
@@ -306,7 +555,14 @@ function normalizeAd(raw) {
|
|
|
306
555
|
height,
|
|
307
556
|
alt: raw.alt == null ? void 0 : String(raw.alt),
|
|
308
557
|
clickUrl: safeUrl(raw.clickUrl) || "",
|
|
309
|
-
trackers: {
|
|
558
|
+
trackers: {
|
|
559
|
+
impression: cleanList(t.impression),
|
|
560
|
+
viewable: cleanList(t.viewable),
|
|
561
|
+
click: cleanList(t.click),
|
|
562
|
+
start: cleanList(t.start),
|
|
563
|
+
quartile: Object.fromEntries(QUARTILE_NAMES.map((n) => [n, cleanList(t.quartile && t.quartile[n])])),
|
|
564
|
+
custom: cleanCustom(t.custom)
|
|
565
|
+
},
|
|
310
566
|
sandbox: typeof raw.sandbox === "string" ? raw.sandbox : void 0
|
|
311
567
|
};
|
|
312
568
|
if (type === "image") {
|
|
@@ -318,6 +574,14 @@ function normalizeAd(raw) {
|
|
|
318
574
|
} else if (type === "html") {
|
|
319
575
|
if (typeof raw.html !== "string" || !raw.html) return { ok: false, reason: "html mancante" };
|
|
320
576
|
ad.html = raw.html;
|
|
577
|
+
} else if (type === "native") {
|
|
578
|
+
const n = normalizeNative(raw.native, safeUrl);
|
|
579
|
+
if (!n.ok) return n;
|
|
580
|
+
ad.native = n.native;
|
|
581
|
+
} else if (type === "video") {
|
|
582
|
+
const v = normalizeVideo(raw, safeUrl);
|
|
583
|
+
if (!v.ok) return v;
|
|
584
|
+
ad.video = v.video;
|
|
321
585
|
} else {
|
|
322
586
|
return { ok: false, reason: "tipo sconosciuto: " + type };
|
|
323
587
|
}
|
|
@@ -352,7 +616,8 @@ window.UALExt={
|
|
|
352
616
|
expand:function(w,h){s({cmd:'resize',w:w,h:h,expanded:true});},
|
|
353
617
|
collapse:function(){s({cmd:'collapse'});},
|
|
354
618
|
close:function(){s({cmd:'close'});},
|
|
355
|
-
click:function(){s({cmd:'click'});if(window.clickTag)window.open(window.clickTag,'_blank');}
|
|
619
|
+
click:function(){s({cmd:'click'});if(window.clickTag)window.open(window.clickTag,'_blank');},
|
|
620
|
+
track:function(n){s({cmd:'track',name:String(n)});}
|
|
356
621
|
};
|
|
357
622
|
})();</script>`;
|
|
358
623
|
}
|
|
@@ -395,6 +660,11 @@ function renderInFrame(container, ad, { slotId, onClick, onClose } = {}) {
|
|
|
395
660
|
fire(ad.trackers && ad.trackers.click);
|
|
396
661
|
if (onClick) onClick();
|
|
397
662
|
},
|
|
663
|
+
// UALExt.track(nome): solo i nomi dichiarati dall'ad server nella risposta
|
|
664
|
+
onTrack: (name) => {
|
|
665
|
+
const custom = ad.trackers && ad.trackers.custom;
|
|
666
|
+
if (typeof name === "string" && custom && Object.hasOwn(custom, name)) fire(custom[name]);
|
|
667
|
+
},
|
|
398
668
|
onClose
|
|
399
669
|
});
|
|
400
670
|
container.innerHTML = "";
|
|
@@ -430,12 +700,42 @@ function onMessage(e) {
|
|
|
430
700
|
case "click":
|
|
431
701
|
f.onClick();
|
|
432
702
|
break;
|
|
703
|
+
case "track":
|
|
704
|
+
f.onTrack(d.name);
|
|
705
|
+
break;
|
|
433
706
|
default:
|
|
434
707
|
}
|
|
435
708
|
}
|
|
436
709
|
|
|
710
|
+
// src/core/sticky.js
|
|
711
|
+
var STICKY_MAX_SCREEN_SHARE = 0.3;
|
|
712
|
+
var STICKY_POSITIONS = ["bottom", "top"];
|
|
713
|
+
function stickyAllowed(adHeight, viewportHeight) {
|
|
714
|
+
return adHeight > 0 && viewportHeight > 0 && adHeight <= viewportHeight * STICKY_MAX_SCREEN_SHARE;
|
|
715
|
+
}
|
|
716
|
+
function stickyPosition(cfg) {
|
|
717
|
+
if (!cfg) return null;
|
|
718
|
+
const p = typeof cfg === "string" ? cfg : cfg && typeof cfg === "object" ? cfg.position : "bottom";
|
|
719
|
+
return STICKY_POSITIONS.includes(p) ? p : "bottom";
|
|
720
|
+
}
|
|
721
|
+
function applySticky(el2, position, onClose) {
|
|
722
|
+
const saved = el2.style.cssText;
|
|
723
|
+
el2.style.cssText += `;position:fixed;left:0;right:0;${position}:0;z-index:2147483000;text-align:center;margin:0;padding:0`;
|
|
724
|
+
const close = document.createElement("button");
|
|
725
|
+
close.type = "button";
|
|
726
|
+
close.textContent = "\xD7";
|
|
727
|
+
close.setAttribute("aria-label", "Close advertisement");
|
|
728
|
+
close.style.cssText = `position:absolute;right:6px;${position === "bottom" ? "top" : "bottom"}:-26px;width:26px;height:26px;border:0;border-radius:6px 6px 0 0;background:rgba(0,0,0,.7);color:#fff;font:16px/26px system-ui,sans-serif;cursor:pointer`;
|
|
729
|
+
close.addEventListener("click", onClose);
|
|
730
|
+
el2.appendChild(close);
|
|
731
|
+
return () => {
|
|
732
|
+
el2.style.cssText = saved;
|
|
733
|
+
close.remove();
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
437
737
|
// src/core/viewability.js
|
|
438
|
-
function watchViewability(
|
|
738
|
+
function watchViewability(el2, { threshold = 0.5, durationMs = 1e3 } = {}, onViewable) {
|
|
439
739
|
if (!("IntersectionObserver" in window)) return () => {
|
|
440
740
|
};
|
|
441
741
|
let timer = null;
|
|
@@ -462,7 +762,7 @@ function watchViewability(el, { threshold = 0.5, durationMs = 1e3 } = {}, onView
|
|
|
462
762
|
}, { threshold: [0, threshold, 1] });
|
|
463
763
|
const onVis = () => document.visibilityState === "visible" && inView ? start() : stop();
|
|
464
764
|
document.addEventListener("visibilitychange", onVis);
|
|
465
|
-
io.observe(
|
|
765
|
+
io.observe(el2);
|
|
466
766
|
function cleanup() {
|
|
467
767
|
stop();
|
|
468
768
|
io.disconnect();
|
|
@@ -556,50 +856,50 @@ function createEngine(adapterRegistry) {
|
|
|
556
856
|
function scan(root) {
|
|
557
857
|
const list = [];
|
|
558
858
|
if (root.nodeType === 1 && root.matches && root.matches("[data-ad-slot]")) list.push(root);
|
|
559
|
-
if (root.querySelectorAll) root.querySelectorAll("[data-ad-slot]").forEach((
|
|
859
|
+
if (root.querySelectorAll) root.querySelectorAll("[data-ad-slot]").forEach((el2) => list.push(el2));
|
|
560
860
|
list.forEach(maybeRegister);
|
|
561
861
|
}
|
|
562
|
-
function maybeRegister(
|
|
563
|
-
const id =
|
|
564
|
-
if (!id) return register(
|
|
862
|
+
function maybeRegister(el2) {
|
|
863
|
+
const id = el2.dataset.ualId;
|
|
864
|
+
if (!id) return register(el2);
|
|
565
865
|
if (slots.has(id)) return;
|
|
566
|
-
if (FINAL_STATUS.has(
|
|
567
|
-
resetEl(
|
|
568
|
-
register(
|
|
866
|
+
if (FINAL_STATUS.has(el2.dataset.ualStatus)) return;
|
|
867
|
+
resetEl(el2);
|
|
868
|
+
register(el2);
|
|
569
869
|
}
|
|
570
|
-
function resetEl(
|
|
571
|
-
delete
|
|
572
|
-
delete
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
870
|
+
function resetEl(el2) {
|
|
871
|
+
delete el2.dataset.ualId;
|
|
872
|
+
delete el2.dataset.ualStatus;
|
|
873
|
+
el2.querySelectorAll(":scope > .ual-label, :scope > .ual-creative").forEach((n) => n.remove());
|
|
874
|
+
el2.classList.remove("ual-slot", "ual-filled");
|
|
875
|
+
el2.style.display = "";
|
|
876
|
+
el2.style.minHeight = "";
|
|
577
877
|
}
|
|
578
|
-
function register(
|
|
579
|
-
const name =
|
|
878
|
+
function register(el2) {
|
|
879
|
+
const name = el2.getAttribute("data-ad-slot");
|
|
580
880
|
const slotCfg = cfg.slots[name];
|
|
581
|
-
|
|
881
|
+
el2.dataset.ualId = name + "#" + ++seq2;
|
|
582
882
|
if (!slotCfg) {
|
|
583
883
|
log("Slot non configurato, ignorato:", name);
|
|
584
|
-
|
|
884
|
+
el2.dataset.ualStatus = "unconfigured";
|
|
585
885
|
return;
|
|
586
886
|
}
|
|
587
887
|
if (slotCfg.enabled === false) {
|
|
588
888
|
log("Slot disabilitato:", name);
|
|
589
|
-
|
|
590
|
-
collapse(
|
|
889
|
+
el2.dataset.ualStatus = "disabled";
|
|
890
|
+
collapse(el2, slotCfg);
|
|
591
891
|
return;
|
|
592
892
|
}
|
|
593
893
|
let params = {};
|
|
594
894
|
try {
|
|
595
|
-
params =
|
|
895
|
+
params = el2.dataset.adParams ? JSON.parse(el2.dataset.adParams) : {};
|
|
596
896
|
} catch (e) {
|
|
597
|
-
warn("data-ad-params non \xE8 JSON valido",
|
|
897
|
+
warn("data-ad-params non \xE8 JSON valido", el2);
|
|
598
898
|
}
|
|
599
899
|
const slot = {
|
|
600
|
-
instanceId:
|
|
900
|
+
instanceId: el2.dataset.ualId,
|
|
601
901
|
name,
|
|
602
|
-
el,
|
|
902
|
+
el: el2,
|
|
603
903
|
cfg: slotCfg,
|
|
604
904
|
params,
|
|
605
905
|
status: "idle",
|
|
@@ -611,23 +911,23 @@ function createEngine(adapterRegistry) {
|
|
|
611
911
|
prepare(slot);
|
|
612
912
|
bus.emit("slotRegistered", publicSlot(slot));
|
|
613
913
|
const lazy = slotCfg.lazy !== void 0 ? slotCfg.lazy : cfg.lazy.enabled;
|
|
614
|
-
if (lazy && lazyIO) lazyIO.observe(
|
|
914
|
+
if (lazy && lazyIO) lazyIO.observe(el2);
|
|
615
915
|
else enqueue(slot);
|
|
616
916
|
}
|
|
617
917
|
function prepare(slot) {
|
|
618
|
-
const { el, cfg: c } = slot;
|
|
619
|
-
if (c.minHeight)
|
|
620
|
-
|
|
918
|
+
const { el: el2, cfg: c } = slot;
|
|
919
|
+
if (c.minHeight) el2.style.minHeight = c.minHeight + "px";
|
|
920
|
+
el2.classList.add("ual-slot");
|
|
621
921
|
if (c.label) {
|
|
622
922
|
const l = document.createElement("div");
|
|
623
923
|
l.className = "ual-label";
|
|
624
924
|
l.textContent = c.label;
|
|
625
925
|
l.style.cssText = "font:10px/1.4 sans-serif;color:#888;text-align:center;text-transform:uppercase;letter-spacing:.05em";
|
|
626
|
-
|
|
926
|
+
el2.appendChild(l);
|
|
627
927
|
}
|
|
628
928
|
const box = document.createElement("div");
|
|
629
929
|
box.className = "ual-creative";
|
|
630
|
-
|
|
930
|
+
el2.appendChild(box);
|
|
631
931
|
slot.box = box;
|
|
632
932
|
}
|
|
633
933
|
function observeDom() {
|
|
@@ -752,7 +1052,7 @@ function createEngine(adapterRegistry) {
|
|
|
752
1052
|
const res = await withTimeout(adapter.request(req, pcfg, c), timeout, {});
|
|
753
1053
|
group.forEach((s, i) => {
|
|
754
1054
|
const raw = res[req[i].id];
|
|
755
|
-
if (!raw || !raw.html && !raw.src && !raw.url) {
|
|
1055
|
+
if (!raw || !raw.html && !raw.src && !raw.url && !raw.native) {
|
|
756
1056
|
unfilled.push(s);
|
|
757
1057
|
return;
|
|
758
1058
|
}
|
|
@@ -764,14 +1064,18 @@ function createEngine(adapterRegistry) {
|
|
|
764
1064
|
return;
|
|
765
1065
|
}
|
|
766
1066
|
const ad = n.ad;
|
|
767
|
-
const
|
|
1067
|
+
const onClick = () => {
|
|
1068
|
+
fire(ad.trackers.click);
|
|
1069
|
+
bus.emit("slotClick", publicSlot(s));
|
|
1070
|
+
};
|
|
1071
|
+
const handle = ad.type === "native" ? renderNative(s.box, ad, { onClick }) : ad.type === "video" ? renderVideo(s.box, ad, {
|
|
1072
|
+
onClick,
|
|
1073
|
+
onEvent: (name) => bus.emit("videoEvent", { ...publicSlot(s), event: name }),
|
|
1074
|
+
onError: (error) => bus.emit("error", { provider: pid, slot: s.name, error })
|
|
1075
|
+
}) : renderInFrame(s.box, ad, {
|
|
768
1076
|
slotId: s.instanceId,
|
|
769
1077
|
onClick: () => bus.emit("slotClick", publicSlot(s)),
|
|
770
|
-
onClose: () =>
|
|
771
|
-
destroySlot(s);
|
|
772
|
-
s.el.dataset.ualStatus = "closed";
|
|
773
|
-
collapse(s.el, s.cfg, true);
|
|
774
|
-
}
|
|
1078
|
+
onClose: () => closeSlot(s)
|
|
775
1079
|
});
|
|
776
1080
|
s.cleanup.push(handle.destroy);
|
|
777
1081
|
fire(ad.trackers.impression);
|
|
@@ -798,6 +1102,14 @@ function createEngine(adapterRegistry) {
|
|
|
798
1102
|
s.el.style.display = "";
|
|
799
1103
|
bus.emit("slotRendered", { ...publicSlot(s), width: info.width, height: info.height, meta: info.meta });
|
|
800
1104
|
const vcfg = { ...cfg.viewability, ...s.cfg.viewability };
|
|
1105
|
+
if (info.ad && info.ad.type === "video" && !(s.cfg.viewability && s.cfg.viewability.durationMs !== void 0)) {
|
|
1106
|
+
vcfg.durationMs = Math.max(vcfg.durationMs, 2e3);
|
|
1107
|
+
}
|
|
1108
|
+
const pos = stickyPosition(s.cfg.sticky);
|
|
1109
|
+
if (pos && info.height) {
|
|
1110
|
+
if (stickyAllowed(info.height, window.innerHeight)) s.cleanup.push(applySticky(s.el, pos, () => closeSlot(s)));
|
|
1111
|
+
else warn(`Slot "${s.name}": sticky ignorato, l'annuncio (${info.height}px) supera il 30% dello schermo`);
|
|
1112
|
+
}
|
|
801
1113
|
const area = (info.width || 0) * (info.height || 0);
|
|
802
1114
|
if (area > LARGE_AD_AREA && !(s.cfg.viewability && s.cfg.viewability.threshold !== void 0)) {
|
|
803
1115
|
vcfg.threshold = Math.min(vcfg.threshold, 0.3);
|
|
@@ -817,11 +1129,11 @@ function createEngine(adapterRegistry) {
|
|
|
817
1129
|
collapse(s.el, s.cfg);
|
|
818
1130
|
bus.emit("slotEmpty", publicSlot(s));
|
|
819
1131
|
}
|
|
820
|
-
function collapse(
|
|
1132
|
+
function collapse(el2, c, force) {
|
|
821
1133
|
const doCollapse = force || (c.collapseEmpty !== void 0 ? c.collapseEmpty : cfg && cfg.collapseEmpty);
|
|
822
1134
|
if (doCollapse) {
|
|
823
|
-
|
|
824
|
-
|
|
1135
|
+
el2.style.display = "none";
|
|
1136
|
+
el2.style.minHeight = "";
|
|
825
1137
|
}
|
|
826
1138
|
}
|
|
827
1139
|
function clear(s) {
|
|
@@ -869,6 +1181,11 @@ function createEngine(adapterRegistry) {
|
|
|
869
1181
|
s.consentSkipped = false;
|
|
870
1182
|
enqueue(s);
|
|
871
1183
|
}
|
|
1184
|
+
function closeSlot(s) {
|
|
1185
|
+
destroySlot(s);
|
|
1186
|
+
s.el.dataset.ualStatus = "closed";
|
|
1187
|
+
collapse(s.el, s.cfg, true);
|
|
1188
|
+
}
|
|
872
1189
|
function destroySlot(s) {
|
|
873
1190
|
callDestroy(s);
|
|
874
1191
|
clear(s);
|
|
@@ -1233,7 +1550,7 @@ var house_default = {
|
|
|
1233
1550
|
};
|
|
1234
1551
|
|
|
1235
1552
|
// src/lib.js
|
|
1236
|
-
var version = true ? "0.
|
|
1553
|
+
var version = true ? "0.2.0" : "dev";
|
|
1237
1554
|
function createUal() {
|
|
1238
1555
|
const registry = new Map(Object.entries({
|
|
1239
1556
|
json: json_default,
|