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/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: { impression: cleanList(t.impression), viewable: cleanList(t.viewable), click: cleanList(t.click) },
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(el, { threshold = 0.5, durationMs = 1e3 } = {}, onViewable) {
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(el);
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((el) => list.push(el));
887
+ if (root.querySelectorAll) root.querySelectorAll("[data-ad-slot]").forEach((el2) => list.push(el2));
588
888
  list.forEach(maybeRegister);
589
889
  }
590
- function maybeRegister(el) {
591
- const id = el.dataset.ualId;
592
- if (!id) return register(el);
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(el.dataset.ualStatus)) return;
595
- resetEl(el);
596
- register(el);
894
+ if (FINAL_STATUS.has(el2.dataset.ualStatus)) return;
895
+ resetEl(el2);
896
+ register(el2);
597
897
  }
598
- function resetEl(el) {
599
- delete el.dataset.ualId;
600
- delete el.dataset.ualStatus;
601
- el.querySelectorAll(":scope > .ual-label, :scope > .ual-creative").forEach((n) => n.remove());
602
- el.classList.remove("ual-slot", "ual-filled");
603
- el.style.display = "";
604
- el.style.minHeight = "";
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(el) {
607
- const name = el.getAttribute("data-ad-slot");
906
+ function register(el2) {
907
+ const name = el2.getAttribute("data-ad-slot");
608
908
  const slotCfg = cfg.slots[name];
609
- el.dataset.ualId = name + "#" + ++seq2;
909
+ el2.dataset.ualId = name + "#" + ++seq2;
610
910
  if (!slotCfg) {
611
911
  log("Slot non configurato, ignorato:", name);
612
- el.dataset.ualStatus = "unconfigured";
912
+ el2.dataset.ualStatus = "unconfigured";
613
913
  return;
614
914
  }
615
915
  if (slotCfg.enabled === false) {
616
916
  log("Slot disabilitato:", name);
617
- el.dataset.ualStatus = "disabled";
618
- collapse(el, slotCfg);
917
+ el2.dataset.ualStatus = "disabled";
918
+ collapse(el2, slotCfg);
619
919
  return;
620
920
  }
621
921
  let params = {};
622
922
  try {
623
- params = el.dataset.adParams ? JSON.parse(el.dataset.adParams) : {};
923
+ params = el2.dataset.adParams ? JSON.parse(el2.dataset.adParams) : {};
624
924
  } catch (e) {
625
- warn("data-ad-params non \xE8 JSON valido", el);
925
+ warn("data-ad-params non \xE8 JSON valido", el2);
626
926
  }
627
927
  const slot = {
628
- instanceId: el.dataset.ualId,
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(el);
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) el.style.minHeight = c.minHeight + "px";
648
- el.classList.add("ual-slot");
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
- el.appendChild(l);
954
+ el2.appendChild(l);
655
955
  }
656
956
  const box = document.createElement("div");
657
957
  box.className = "ual-creative";
658
- el.appendChild(box);
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 handle = renderInFrame(s.box, ad, {
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(el, c, force) {
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
- el.style.display = "none";
852
- el.style.minHeight = "";
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.1.1" : "dev";
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
- width: number;
239
- height: number;
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?: { impression?: string[]; viewable?: string[]; click?: string[] };
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