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