universal-ad-loader 0.1.1 → 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 +24 -0
- package/README.md +58 -4
- 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.cjs
CHANGED
|
@@ -299,6 +299,247 @@ function normalize(tc, opts) {
|
|
|
299
299
|
return out;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
// src/core/native.js
|
|
303
|
+
var NATIVE_LIMITS = { title: 150, body: 300, sponsoredBy: 60, cta: 30, displayUrl: 100, price: 30, label: 20 };
|
|
304
|
+
var NATIVE_LAYOUTS = ["card", "list", "compact"];
|
|
305
|
+
var clip = (v, n) => typeof v === "string" ? v.trim().slice(0, n) : "";
|
|
306
|
+
function normalizeNative(raw, safeUrl2) {
|
|
307
|
+
if (!raw || typeof raw !== "object") return { ok: false, reason: "native mancante" };
|
|
308
|
+
const title = clip(raw.title, NATIVE_LIMITS.title);
|
|
309
|
+
if (!title) return { ok: false, reason: "native: titolo mancante" };
|
|
310
|
+
const img = (v) => {
|
|
311
|
+
const o = typeof v === "string" ? { src: v } : v;
|
|
312
|
+
const src = o && typeof o === "object" ? safeUrl2(o.src, true) : null;
|
|
313
|
+
if (!src) return null;
|
|
314
|
+
const dim2 = (n) => {
|
|
315
|
+
const x = Math.round(Number(n));
|
|
316
|
+
return x > 0 && x <= 2e3 ? x : 0;
|
|
317
|
+
};
|
|
318
|
+
return { src, width: dim2(o.width), height: dim2(o.height) };
|
|
319
|
+
};
|
|
320
|
+
const rating = Number(raw.rating);
|
|
321
|
+
return {
|
|
322
|
+
ok: true,
|
|
323
|
+
native: {
|
|
324
|
+
title,
|
|
325
|
+
body: clip(raw.body, NATIVE_LIMITS.body),
|
|
326
|
+
sponsoredBy: clip(raw.sponsoredBy, NATIVE_LIMITS.sponsoredBy),
|
|
327
|
+
cta: clip(raw.cta, NATIVE_LIMITS.cta),
|
|
328
|
+
displayUrl: clip(raw.displayUrl, NATIVE_LIMITS.displayUrl),
|
|
329
|
+
price: clip(raw.price, NATIVE_LIMITS.price),
|
|
330
|
+
rating: Number.isFinite(rating) && rating >= 0 && rating <= 5 ? rating : null,
|
|
331
|
+
image: img(raw.image),
|
|
332
|
+
icon: img(raw.icon),
|
|
333
|
+
layout: NATIVE_LAYOUTS.includes(raw.layout) ? raw.layout : "card",
|
|
334
|
+
label: clip(raw.label, NATIVE_LIMITS.label) || "Sponsored"
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
var STYLE = `
|
|
339
|
+
:host{display:block;max-width:100%;font:inherit;color:inherit;line-height:1.35}
|
|
340
|
+
*{box-sizing:border-box}
|
|
341
|
+
.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}
|
|
342
|
+
a.card:hover .title{text-decoration:underline}
|
|
343
|
+
.label{font-size:.72em;letter-spacing:.04em;text-transform:uppercase;opacity:.75}
|
|
344
|
+
.main{display:block;width:100%;height:auto;border-radius:calc(var(--ual-native-radius,8px) - 2px);background:rgba(128,128,128,.12)}
|
|
345
|
+
.row{display:flex;gap:.6em;align-items:flex-start}
|
|
346
|
+
.icon{width:2.4em;height:2.4em;flex:none;border-radius:6px;object-fit:cover}
|
|
347
|
+
.title{font-weight:700;font-size:1.05em}
|
|
348
|
+
.body{opacity:.9;font-size:.92em}
|
|
349
|
+
.meta{font-size:.8em;opacity:.7}
|
|
350
|
+
.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)}
|
|
351
|
+
.price{font-weight:600}
|
|
352
|
+
.layout-list{flex-direction:row;align-items:flex-start}
|
|
353
|
+
.layout-list .main{width:35%;max-width:180px;flex:none}
|
|
354
|
+
.layout-list .text{display:flex;flex-direction:column;gap:.35em;min-width:0}
|
|
355
|
+
.layout-compact .main{display:none}
|
|
356
|
+
`;
|
|
357
|
+
function el(tag, cls, text) {
|
|
358
|
+
const e = document.createElement(tag);
|
|
359
|
+
if (cls) e.className = cls;
|
|
360
|
+
if (text != null) e.textContent = text;
|
|
361
|
+
return e;
|
|
362
|
+
}
|
|
363
|
+
function image(i, cls, alt) {
|
|
364
|
+
const im = el("img", cls);
|
|
365
|
+
im.src = i.src;
|
|
366
|
+
im.alt = alt || "";
|
|
367
|
+
im.loading = "lazy";
|
|
368
|
+
im.referrerPolicy = "strict-origin-when-cross-origin";
|
|
369
|
+
if (i.width && i.height) {
|
|
370
|
+
im.width = i.width;
|
|
371
|
+
im.height = i.height;
|
|
372
|
+
}
|
|
373
|
+
return im;
|
|
374
|
+
}
|
|
375
|
+
function renderNative(container, ad, { onClick } = {}) {
|
|
376
|
+
const n = ad.native;
|
|
377
|
+
const host = el("div", "ual-native");
|
|
378
|
+
const root = host.attachShadow ? host.attachShadow({ mode: "open" }) : host;
|
|
379
|
+
const style = document.createElement("style");
|
|
380
|
+
style.textContent = STYLE;
|
|
381
|
+
root.appendChild(style);
|
|
382
|
+
const card = ad.clickUrl ? el("a", `card layout-${n.layout}`) : el("div", `card layout-${n.layout}`);
|
|
383
|
+
if (ad.clickUrl) {
|
|
384
|
+
card.href = ad.clickUrl;
|
|
385
|
+
card.target = "_blank";
|
|
386
|
+
card.rel = "noopener noreferrer sponsored nofollow";
|
|
387
|
+
card.addEventListener("click", () => {
|
|
388
|
+
if (onClick) onClick();
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
card.appendChild(el("span", "label", n.label));
|
|
392
|
+
if (n.image) card.appendChild(image(n.image, "main", n.title));
|
|
393
|
+
const text = el("div", "text");
|
|
394
|
+
const head = el("div", "row");
|
|
395
|
+
if (n.icon) head.appendChild(image(n.icon, "icon", ""));
|
|
396
|
+
const titles = el("div");
|
|
397
|
+
titles.appendChild(el("div", "title", n.title));
|
|
398
|
+
if (n.sponsoredBy) titles.appendChild(el("div", "meta", n.sponsoredBy));
|
|
399
|
+
head.appendChild(titles);
|
|
400
|
+
text.appendChild(head);
|
|
401
|
+
if (n.body) text.appendChild(el("div", "body", n.body));
|
|
402
|
+
const meta = [n.displayUrl, n.rating !== null ? `\u2605 ${n.rating}` : ""].filter(Boolean).join(" \xB7 ");
|
|
403
|
+
if (meta) text.appendChild(el("div", "meta", meta));
|
|
404
|
+
if (n.price) text.appendChild(el("div", "price", n.price));
|
|
405
|
+
if (n.cta) text.appendChild(el("span", "cta", n.cta));
|
|
406
|
+
card.appendChild(text);
|
|
407
|
+
root.appendChild(card);
|
|
408
|
+
container.innerHTML = "";
|
|
409
|
+
container.appendChild(host);
|
|
410
|
+
return { el: host, destroy: () => host.remove() };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/core/video.js
|
|
414
|
+
var QUARTILES = [["firstQuartile", 0.25], ["midpoint", 0.5], ["thirdQuartile", 0.75]];
|
|
415
|
+
var QUARTILE_NAMES = ["firstQuartile", "midpoint", "thirdQuartile", "complete"];
|
|
416
|
+
var PLAY_THRESHOLD = 0.5;
|
|
417
|
+
function quartileHits(fired, currentTime, duration) {
|
|
418
|
+
if (!(duration > 0) || !(currentTime >= 0)) return [];
|
|
419
|
+
const f = currentTime / duration;
|
|
420
|
+
const hits = [];
|
|
421
|
+
for (const [name, at] of QUARTILES) {
|
|
422
|
+
if (f >= at && !fired.has(name)) {
|
|
423
|
+
fired.add(name);
|
|
424
|
+
hits.push(name);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return hits;
|
|
428
|
+
}
|
|
429
|
+
function normalizeVideo(raw, safeUrl2) {
|
|
430
|
+
const src = safeUrl2(raw.src);
|
|
431
|
+
if (!src) return { ok: false, reason: "video: src non valido (ammessi solo http e https)" };
|
|
432
|
+
return { ok: true, video: { src, poster: safeUrl2(raw.poster, true) || "", mime: typeof raw.mime === "string" && /^video\/[\w.+-]+$/.test(raw.mime) ? raw.mime : "" } };
|
|
433
|
+
}
|
|
434
|
+
function btn(label) {
|
|
435
|
+
const b = document.createElement("button");
|
|
436
|
+
b.type = "button";
|
|
437
|
+
b.textContent = label;
|
|
438
|
+
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";
|
|
439
|
+
return b;
|
|
440
|
+
}
|
|
441
|
+
function renderVideo(container, ad, { onClick, onEvent, onError } = {}) {
|
|
442
|
+
const wrap = document.createElement("div");
|
|
443
|
+
wrap.className = "ual-video";
|
|
444
|
+
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`;
|
|
445
|
+
const v = document.createElement("video");
|
|
446
|
+
v.muted = true;
|
|
447
|
+
v.defaultMuted = true;
|
|
448
|
+
v.playsInline = true;
|
|
449
|
+
v.setAttribute("playsinline", "");
|
|
450
|
+
v.preload = "metadata";
|
|
451
|
+
v.disablePictureInPicture = true;
|
|
452
|
+
v.controls = false;
|
|
453
|
+
v.style.cssText = "display:block;width:100%;height:100%;object-fit:contain";
|
|
454
|
+
if (ad.video.poster) v.poster = ad.video.poster;
|
|
455
|
+
const source = document.createElement("source");
|
|
456
|
+
source.src = ad.video.src;
|
|
457
|
+
if (ad.video.mime) source.type = ad.video.mime;
|
|
458
|
+
v.appendChild(source);
|
|
459
|
+
wrap.appendChild(v);
|
|
460
|
+
const label = document.createElement("span");
|
|
461
|
+
label.textContent = "Advertisement";
|
|
462
|
+
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";
|
|
463
|
+
wrap.appendChild(label);
|
|
464
|
+
if (ad.clickUrl) {
|
|
465
|
+
const a = document.createElement("a");
|
|
466
|
+
a.href = ad.clickUrl;
|
|
467
|
+
a.target = "_blank";
|
|
468
|
+
a.rel = "noopener noreferrer sponsored nofollow";
|
|
469
|
+
a.setAttribute("aria-label", "Open the advertiser page");
|
|
470
|
+
a.style.cssText = "position:absolute;inset:0;z-index:1";
|
|
471
|
+
a.addEventListener("click", () => {
|
|
472
|
+
if (onClick) onClick();
|
|
473
|
+
});
|
|
474
|
+
wrap.appendChild(a);
|
|
475
|
+
}
|
|
476
|
+
const sound = btn("Sound on");
|
|
477
|
+
sound.setAttribute("aria-label", "Turn sound on");
|
|
478
|
+
sound.addEventListener("click", () => {
|
|
479
|
+
v.muted = !v.muted;
|
|
480
|
+
sound.textContent = v.muted ? "Sound on" : "Sound off";
|
|
481
|
+
sound.setAttribute("aria-label", v.muted ? "Turn sound on" : "Turn sound off");
|
|
482
|
+
});
|
|
483
|
+
wrap.appendChild(sound);
|
|
484
|
+
const trackers = ad.trackers || {};
|
|
485
|
+
const fired = /* @__PURE__ */ new Set();
|
|
486
|
+
let started = false;
|
|
487
|
+
const emit = (name, urls) => {
|
|
488
|
+
fire(urls);
|
|
489
|
+
if (onEvent) onEvent(name);
|
|
490
|
+
};
|
|
491
|
+
v.addEventListener("playing", () => {
|
|
492
|
+
if (!started) {
|
|
493
|
+
started = true;
|
|
494
|
+
emit("start", trackers.start);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
v.addEventListener("timeupdate", () => {
|
|
498
|
+
quartileHits(fired, v.currentTime, v.duration).forEach((n) => emit(n, trackers.quartile && trackers.quartile[n]));
|
|
499
|
+
});
|
|
500
|
+
v.addEventListener("ended", () => {
|
|
501
|
+
quartileHits(fired, v.duration, v.duration).forEach((n) => emit(n, trackers.quartile && trackers.quartile[n]));
|
|
502
|
+
if (!fired.has("complete")) {
|
|
503
|
+
fired.add("complete");
|
|
504
|
+
emit("complete", trackers.quartile && trackers.quartile.complete);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
v.addEventListener("error", () => {
|
|
508
|
+
if (onError) onError(new Error("video: file non riproducibile"));
|
|
509
|
+
});
|
|
510
|
+
let inView = false;
|
|
511
|
+
const sync = () => {
|
|
512
|
+
if (v.ended) return;
|
|
513
|
+
if (inView && document.visibilityState === "visible") {
|
|
514
|
+
const p = v.play();
|
|
515
|
+
if (p && p.catch) p.catch(() => {
|
|
516
|
+
});
|
|
517
|
+
} else v.pause();
|
|
518
|
+
};
|
|
519
|
+
const io = "IntersectionObserver" in window ? new IntersectionObserver((entries) => {
|
|
520
|
+
entries.forEach((en) => {
|
|
521
|
+
inView = en.intersectionRatio >= PLAY_THRESHOLD;
|
|
522
|
+
});
|
|
523
|
+
sync();
|
|
524
|
+
}, { threshold: [0, PLAY_THRESHOLD, 1] }) : null;
|
|
525
|
+
if (io) io.observe(wrap);
|
|
526
|
+
document.addEventListener("visibilitychange", sync);
|
|
527
|
+
container.innerHTML = "";
|
|
528
|
+
container.appendChild(wrap);
|
|
529
|
+
return {
|
|
530
|
+
el: wrap,
|
|
531
|
+
destroy: () => {
|
|
532
|
+
if (io) io.disconnect();
|
|
533
|
+
document.removeEventListener("visibilitychange", sync);
|
|
534
|
+
v.pause();
|
|
535
|
+
v.removeAttribute("src");
|
|
536
|
+
source.remove();
|
|
537
|
+
v.load();
|
|
538
|
+
wrap.remove();
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
302
543
|
// src/core/render.js
|
|
303
544
|
var DEFAULT_SANDBOX = "allow-scripts allow-popups allow-popups-to-escape-sandbox";
|
|
304
545
|
var MAX_DIM = 2e3;
|
|
@@ -321,12 +562,20 @@ function safeUrl(u, allowDataImage) {
|
|
|
321
562
|
function cleanList(list) {
|
|
322
563
|
return (Array.isArray(list) ? list : []).map((u) => safeUrl(u)).filter(Boolean);
|
|
323
564
|
}
|
|
565
|
+
function cleanCustom(custom) {
|
|
566
|
+
const out = {};
|
|
567
|
+
if (!custom || typeof custom !== "object") return out;
|
|
568
|
+
Object.keys(custom).slice(0, 20).forEach((name) => {
|
|
569
|
+
if (/^[\w-]{1,40}$/.test(name)) out[name] = cleanList(custom[name]);
|
|
570
|
+
});
|
|
571
|
+
return out;
|
|
572
|
+
}
|
|
324
573
|
function normalizeAd(raw) {
|
|
325
574
|
if (!raw || typeof raw !== "object") return { ok: false, reason: "annuncio non valido" };
|
|
326
575
|
const type = raw.type || (raw.url ? "url" : raw.src ? "image" : "html");
|
|
327
576
|
const width = dim(raw.width);
|
|
328
577
|
const height = dim(raw.height);
|
|
329
|
-
if (!width || !height) return { ok: false, reason: `dimensioni non valide (${raw.width}x${raw.height}, max ${MAX_DIM})` };
|
|
578
|
+
if (type !== "native" && (!width || !height)) return { ok: false, reason: `dimensioni non valide (${raw.width}x${raw.height}, max ${MAX_DIM})` };
|
|
330
579
|
const t = raw.trackers || {};
|
|
331
580
|
const ad = {
|
|
332
581
|
type,
|
|
@@ -334,7 +583,14 @@ function normalizeAd(raw) {
|
|
|
334
583
|
height,
|
|
335
584
|
alt: raw.alt == null ? void 0 : String(raw.alt),
|
|
336
585
|
clickUrl: safeUrl(raw.clickUrl) || "",
|
|
337
|
-
trackers: {
|
|
586
|
+
trackers: {
|
|
587
|
+
impression: cleanList(t.impression),
|
|
588
|
+
viewable: cleanList(t.viewable),
|
|
589
|
+
click: cleanList(t.click),
|
|
590
|
+
start: cleanList(t.start),
|
|
591
|
+
quartile: Object.fromEntries(QUARTILE_NAMES.map((n) => [n, cleanList(t.quartile && t.quartile[n])])),
|
|
592
|
+
custom: cleanCustom(t.custom)
|
|
593
|
+
},
|
|
338
594
|
sandbox: typeof raw.sandbox === "string" ? raw.sandbox : void 0
|
|
339
595
|
};
|
|
340
596
|
if (type === "image") {
|
|
@@ -346,6 +602,14 @@ function normalizeAd(raw) {
|
|
|
346
602
|
} else if (type === "html") {
|
|
347
603
|
if (typeof raw.html !== "string" || !raw.html) return { ok: false, reason: "html mancante" };
|
|
348
604
|
ad.html = raw.html;
|
|
605
|
+
} else if (type === "native") {
|
|
606
|
+
const n = normalizeNative(raw.native, safeUrl);
|
|
607
|
+
if (!n.ok) return n;
|
|
608
|
+
ad.native = n.native;
|
|
609
|
+
} else if (type === "video") {
|
|
610
|
+
const v = normalizeVideo(raw, safeUrl);
|
|
611
|
+
if (!v.ok) return v;
|
|
612
|
+
ad.video = v.video;
|
|
349
613
|
} else {
|
|
350
614
|
return { ok: false, reason: "tipo sconosciuto: " + type };
|
|
351
615
|
}
|
|
@@ -380,7 +644,8 @@ window.UALExt={
|
|
|
380
644
|
expand:function(w,h){s({cmd:'resize',w:w,h:h,expanded:true});},
|
|
381
645
|
collapse:function(){s({cmd:'collapse'});},
|
|
382
646
|
close:function(){s({cmd:'close'});},
|
|
383
|
-
click:function(){s({cmd:'click'});if(window.clickTag)window.open(window.clickTag,'_blank');}
|
|
647
|
+
click:function(){s({cmd:'click'});if(window.clickTag)window.open(window.clickTag,'_blank');},
|
|
648
|
+
track:function(n){s({cmd:'track',name:String(n)});}
|
|
384
649
|
};
|
|
385
650
|
})();</script>`;
|
|
386
651
|
}
|
|
@@ -423,6 +688,11 @@ function renderInFrame(container, ad, { slotId, onClick, onClose } = {}) {
|
|
|
423
688
|
fire(ad.trackers && ad.trackers.click);
|
|
424
689
|
if (onClick) onClick();
|
|
425
690
|
},
|
|
691
|
+
// UALExt.track(nome): solo i nomi dichiarati dall'ad server nella risposta
|
|
692
|
+
onTrack: (name) => {
|
|
693
|
+
const custom = ad.trackers && ad.trackers.custom;
|
|
694
|
+
if (typeof name === "string" && custom && Object.hasOwn(custom, name)) fire(custom[name]);
|
|
695
|
+
},
|
|
426
696
|
onClose
|
|
427
697
|
});
|
|
428
698
|
container.innerHTML = "";
|
|
@@ -458,12 +728,42 @@ function onMessage(e) {
|
|
|
458
728
|
case "click":
|
|
459
729
|
f.onClick();
|
|
460
730
|
break;
|
|
731
|
+
case "track":
|
|
732
|
+
f.onTrack(d.name);
|
|
733
|
+
break;
|
|
461
734
|
default:
|
|
462
735
|
}
|
|
463
736
|
}
|
|
464
737
|
|
|
738
|
+
// src/core/sticky.js
|
|
739
|
+
var STICKY_MAX_SCREEN_SHARE = 0.3;
|
|
740
|
+
var STICKY_POSITIONS = ["bottom", "top"];
|
|
741
|
+
function stickyAllowed(adHeight, viewportHeight) {
|
|
742
|
+
return adHeight > 0 && viewportHeight > 0 && adHeight <= viewportHeight * STICKY_MAX_SCREEN_SHARE;
|
|
743
|
+
}
|
|
744
|
+
function stickyPosition(cfg) {
|
|
745
|
+
if (!cfg) return null;
|
|
746
|
+
const p = typeof cfg === "string" ? cfg : cfg && typeof cfg === "object" ? cfg.position : "bottom";
|
|
747
|
+
return STICKY_POSITIONS.includes(p) ? p : "bottom";
|
|
748
|
+
}
|
|
749
|
+
function applySticky(el2, position, onClose) {
|
|
750
|
+
const saved = el2.style.cssText;
|
|
751
|
+
el2.style.cssText += `;position:fixed;left:0;right:0;${position}:0;z-index:2147483000;text-align:center;margin:0;padding:0`;
|
|
752
|
+
const close = document.createElement("button");
|
|
753
|
+
close.type = "button";
|
|
754
|
+
close.textContent = "\xD7";
|
|
755
|
+
close.setAttribute("aria-label", "Close advertisement");
|
|
756
|
+
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`;
|
|
757
|
+
close.addEventListener("click", onClose);
|
|
758
|
+
el2.appendChild(close);
|
|
759
|
+
return () => {
|
|
760
|
+
el2.style.cssText = saved;
|
|
761
|
+
close.remove();
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
465
765
|
// src/core/viewability.js
|
|
466
|
-
function watchViewability(
|
|
766
|
+
function watchViewability(el2, { threshold = 0.5, durationMs = 1e3 } = {}, onViewable) {
|
|
467
767
|
if (!("IntersectionObserver" in window)) return () => {
|
|
468
768
|
};
|
|
469
769
|
let timer = null;
|
|
@@ -490,7 +790,7 @@ function watchViewability(el, { threshold = 0.5, durationMs = 1e3 } = {}, onView
|
|
|
490
790
|
}, { threshold: [0, threshold, 1] });
|
|
491
791
|
const onVis = () => document.visibilityState === "visible" && inView ? start() : stop();
|
|
492
792
|
document.addEventListener("visibilitychange", onVis);
|
|
493
|
-
io.observe(
|
|
793
|
+
io.observe(el2);
|
|
494
794
|
function cleanup() {
|
|
495
795
|
stop();
|
|
496
796
|
io.disconnect();
|
|
@@ -584,50 +884,50 @@ function createEngine(adapterRegistry) {
|
|
|
584
884
|
function scan(root) {
|
|
585
885
|
const list = [];
|
|
586
886
|
if (root.nodeType === 1 && root.matches && root.matches("[data-ad-slot]")) list.push(root);
|
|
587
|
-
if (root.querySelectorAll) root.querySelectorAll("[data-ad-slot]").forEach((
|
|
887
|
+
if (root.querySelectorAll) root.querySelectorAll("[data-ad-slot]").forEach((el2) => list.push(el2));
|
|
588
888
|
list.forEach(maybeRegister);
|
|
589
889
|
}
|
|
590
|
-
function maybeRegister(
|
|
591
|
-
const id =
|
|
592
|
-
if (!id) return register(
|
|
890
|
+
function maybeRegister(el2) {
|
|
891
|
+
const id = el2.dataset.ualId;
|
|
892
|
+
if (!id) return register(el2);
|
|
593
893
|
if (slots.has(id)) return;
|
|
594
|
-
if (FINAL_STATUS.has(
|
|
595
|
-
resetEl(
|
|
596
|
-
register(
|
|
894
|
+
if (FINAL_STATUS.has(el2.dataset.ualStatus)) return;
|
|
895
|
+
resetEl(el2);
|
|
896
|
+
register(el2);
|
|
597
897
|
}
|
|
598
|
-
function resetEl(
|
|
599
|
-
delete
|
|
600
|
-
delete
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
898
|
+
function resetEl(el2) {
|
|
899
|
+
delete el2.dataset.ualId;
|
|
900
|
+
delete el2.dataset.ualStatus;
|
|
901
|
+
el2.querySelectorAll(":scope > .ual-label, :scope > .ual-creative").forEach((n) => n.remove());
|
|
902
|
+
el2.classList.remove("ual-slot", "ual-filled");
|
|
903
|
+
el2.style.display = "";
|
|
904
|
+
el2.style.minHeight = "";
|
|
605
905
|
}
|
|
606
|
-
function register(
|
|
607
|
-
const name =
|
|
906
|
+
function register(el2) {
|
|
907
|
+
const name = el2.getAttribute("data-ad-slot");
|
|
608
908
|
const slotCfg = cfg.slots[name];
|
|
609
|
-
|
|
909
|
+
el2.dataset.ualId = name + "#" + ++seq2;
|
|
610
910
|
if (!slotCfg) {
|
|
611
911
|
log("Slot non configurato, ignorato:", name);
|
|
612
|
-
|
|
912
|
+
el2.dataset.ualStatus = "unconfigured";
|
|
613
913
|
return;
|
|
614
914
|
}
|
|
615
915
|
if (slotCfg.enabled === false) {
|
|
616
916
|
log("Slot disabilitato:", name);
|
|
617
|
-
|
|
618
|
-
collapse(
|
|
917
|
+
el2.dataset.ualStatus = "disabled";
|
|
918
|
+
collapse(el2, slotCfg);
|
|
619
919
|
return;
|
|
620
920
|
}
|
|
621
921
|
let params = {};
|
|
622
922
|
try {
|
|
623
|
-
params =
|
|
923
|
+
params = el2.dataset.adParams ? JSON.parse(el2.dataset.adParams) : {};
|
|
624
924
|
} catch (e) {
|
|
625
|
-
warn("data-ad-params non \xE8 JSON valido",
|
|
925
|
+
warn("data-ad-params non \xE8 JSON valido", el2);
|
|
626
926
|
}
|
|
627
927
|
const slot = {
|
|
628
|
-
instanceId:
|
|
928
|
+
instanceId: el2.dataset.ualId,
|
|
629
929
|
name,
|
|
630
|
-
el,
|
|
930
|
+
el: el2,
|
|
631
931
|
cfg: slotCfg,
|
|
632
932
|
params,
|
|
633
933
|
status: "idle",
|
|
@@ -639,23 +939,23 @@ function createEngine(adapterRegistry) {
|
|
|
639
939
|
prepare(slot);
|
|
640
940
|
bus.emit("slotRegistered", publicSlot(slot));
|
|
641
941
|
const lazy = slotCfg.lazy !== void 0 ? slotCfg.lazy : cfg.lazy.enabled;
|
|
642
|
-
if (lazy && lazyIO) lazyIO.observe(
|
|
942
|
+
if (lazy && lazyIO) lazyIO.observe(el2);
|
|
643
943
|
else enqueue(slot);
|
|
644
944
|
}
|
|
645
945
|
function prepare(slot) {
|
|
646
|
-
const { el, cfg: c } = slot;
|
|
647
|
-
if (c.minHeight)
|
|
648
|
-
|
|
946
|
+
const { el: el2, cfg: c } = slot;
|
|
947
|
+
if (c.minHeight) el2.style.minHeight = c.minHeight + "px";
|
|
948
|
+
el2.classList.add("ual-slot");
|
|
649
949
|
if (c.label) {
|
|
650
950
|
const l = document.createElement("div");
|
|
651
951
|
l.className = "ual-label";
|
|
652
952
|
l.textContent = c.label;
|
|
653
953
|
l.style.cssText = "font:10px/1.4 sans-serif;color:#888;text-align:center;text-transform:uppercase;letter-spacing:.05em";
|
|
654
|
-
|
|
954
|
+
el2.appendChild(l);
|
|
655
955
|
}
|
|
656
956
|
const box = document.createElement("div");
|
|
657
957
|
box.className = "ual-creative";
|
|
658
|
-
|
|
958
|
+
el2.appendChild(box);
|
|
659
959
|
slot.box = box;
|
|
660
960
|
}
|
|
661
961
|
function observeDom() {
|
|
@@ -780,7 +1080,7 @@ function createEngine(adapterRegistry) {
|
|
|
780
1080
|
const res = await withTimeout(adapter.request(req, pcfg, c), timeout, {});
|
|
781
1081
|
group.forEach((s, i) => {
|
|
782
1082
|
const raw = res[req[i].id];
|
|
783
|
-
if (!raw || !raw.html && !raw.src && !raw.url) {
|
|
1083
|
+
if (!raw || !raw.html && !raw.src && !raw.url && !raw.native) {
|
|
784
1084
|
unfilled.push(s);
|
|
785
1085
|
return;
|
|
786
1086
|
}
|
|
@@ -792,14 +1092,18 @@ function createEngine(adapterRegistry) {
|
|
|
792
1092
|
return;
|
|
793
1093
|
}
|
|
794
1094
|
const ad = n.ad;
|
|
795
|
-
const
|
|
1095
|
+
const onClick = () => {
|
|
1096
|
+
fire(ad.trackers.click);
|
|
1097
|
+
bus.emit("slotClick", publicSlot(s));
|
|
1098
|
+
};
|
|
1099
|
+
const handle = ad.type === "native" ? renderNative(s.box, ad, { onClick }) : ad.type === "video" ? renderVideo(s.box, ad, {
|
|
1100
|
+
onClick,
|
|
1101
|
+
onEvent: (name) => bus.emit("videoEvent", { ...publicSlot(s), event: name }),
|
|
1102
|
+
onError: (error) => bus.emit("error", { provider: pid, slot: s.name, error })
|
|
1103
|
+
}) : renderInFrame(s.box, ad, {
|
|
796
1104
|
slotId: s.instanceId,
|
|
797
1105
|
onClick: () => bus.emit("slotClick", publicSlot(s)),
|
|
798
|
-
onClose: () =>
|
|
799
|
-
destroySlot(s);
|
|
800
|
-
s.el.dataset.ualStatus = "closed";
|
|
801
|
-
collapse(s.el, s.cfg, true);
|
|
802
|
-
}
|
|
1106
|
+
onClose: () => closeSlot(s)
|
|
803
1107
|
});
|
|
804
1108
|
s.cleanup.push(handle.destroy);
|
|
805
1109
|
fire(ad.trackers.impression);
|
|
@@ -826,6 +1130,14 @@ function createEngine(adapterRegistry) {
|
|
|
826
1130
|
s.el.style.display = "";
|
|
827
1131
|
bus.emit("slotRendered", { ...publicSlot(s), width: info.width, height: info.height, meta: info.meta });
|
|
828
1132
|
const vcfg = { ...cfg.viewability, ...s.cfg.viewability };
|
|
1133
|
+
if (info.ad && info.ad.type === "video" && !(s.cfg.viewability && s.cfg.viewability.durationMs !== void 0)) {
|
|
1134
|
+
vcfg.durationMs = Math.max(vcfg.durationMs, 2e3);
|
|
1135
|
+
}
|
|
1136
|
+
const pos = stickyPosition(s.cfg.sticky);
|
|
1137
|
+
if (pos && info.height) {
|
|
1138
|
+
if (stickyAllowed(info.height, window.innerHeight)) s.cleanup.push(applySticky(s.el, pos, () => closeSlot(s)));
|
|
1139
|
+
else warn(`Slot "${s.name}": sticky ignorato, l'annuncio (${info.height}px) supera il 30% dello schermo`);
|
|
1140
|
+
}
|
|
829
1141
|
const area = (info.width || 0) * (info.height || 0);
|
|
830
1142
|
if (area > LARGE_AD_AREA && !(s.cfg.viewability && s.cfg.viewability.threshold !== void 0)) {
|
|
831
1143
|
vcfg.threshold = Math.min(vcfg.threshold, 0.3);
|
|
@@ -845,11 +1157,11 @@ function createEngine(adapterRegistry) {
|
|
|
845
1157
|
collapse(s.el, s.cfg);
|
|
846
1158
|
bus.emit("slotEmpty", publicSlot(s));
|
|
847
1159
|
}
|
|
848
|
-
function collapse(
|
|
1160
|
+
function collapse(el2, c, force) {
|
|
849
1161
|
const doCollapse = force || (c.collapseEmpty !== void 0 ? c.collapseEmpty : cfg && cfg.collapseEmpty);
|
|
850
1162
|
if (doCollapse) {
|
|
851
|
-
|
|
852
|
-
|
|
1163
|
+
el2.style.display = "none";
|
|
1164
|
+
el2.style.minHeight = "";
|
|
853
1165
|
}
|
|
854
1166
|
}
|
|
855
1167
|
function clear(s) {
|
|
@@ -897,6 +1209,11 @@ function createEngine(adapterRegistry) {
|
|
|
897
1209
|
s.consentSkipped = false;
|
|
898
1210
|
enqueue(s);
|
|
899
1211
|
}
|
|
1212
|
+
function closeSlot(s) {
|
|
1213
|
+
destroySlot(s);
|
|
1214
|
+
s.el.dataset.ualStatus = "closed";
|
|
1215
|
+
collapse(s.el, s.cfg, true);
|
|
1216
|
+
}
|
|
900
1217
|
function destroySlot(s) {
|
|
901
1218
|
callDestroy(s);
|
|
902
1219
|
clear(s);
|
|
@@ -1261,7 +1578,7 @@ var house_default = {
|
|
|
1261
1578
|
};
|
|
1262
1579
|
|
|
1263
1580
|
// src/lib.js
|
|
1264
|
-
var version = true ? "0.
|
|
1581
|
+
var version = true ? "0.2.0" : "dev";
|
|
1265
1582
|
function createUal() {
|
|
1266
1583
|
const registry = new Map(Object.entries({
|
|
1267
1584
|
json: json_default,
|
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
|
|