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.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: { impression: cleanList(t.impression), viewable: cleanList(t.viewable), click: cleanList(t.click) },
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(el, { threshold = 0.5, durationMs = 1e3 } = {}, onViewable) {
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(el);
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((el) => list.push(el));
859
+ if (root.querySelectorAll) root.querySelectorAll("[data-ad-slot]").forEach((el2) => list.push(el2));
560
860
  list.forEach(maybeRegister);
561
861
  }
562
- function maybeRegister(el) {
563
- const id = el.dataset.ualId;
564
- if (!id) return register(el);
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(el.dataset.ualStatus)) return;
567
- resetEl(el);
568
- register(el);
866
+ if (FINAL_STATUS.has(el2.dataset.ualStatus)) return;
867
+ resetEl(el2);
868
+ register(el2);
569
869
  }
570
- function resetEl(el) {
571
- delete el.dataset.ualId;
572
- delete el.dataset.ualStatus;
573
- el.querySelectorAll(":scope > .ual-label, :scope > .ual-creative").forEach((n) => n.remove());
574
- el.classList.remove("ual-slot", "ual-filled");
575
- el.style.display = "";
576
- el.style.minHeight = "";
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(el) {
579
- const name = el.getAttribute("data-ad-slot");
878
+ function register(el2) {
879
+ const name = el2.getAttribute("data-ad-slot");
580
880
  const slotCfg = cfg.slots[name];
581
- el.dataset.ualId = name + "#" + ++seq2;
881
+ el2.dataset.ualId = name + "#" + ++seq2;
582
882
  if (!slotCfg) {
583
883
  log("Slot non configurato, ignorato:", name);
584
- el.dataset.ualStatus = "unconfigured";
884
+ el2.dataset.ualStatus = "unconfigured";
585
885
  return;
586
886
  }
587
887
  if (slotCfg.enabled === false) {
588
888
  log("Slot disabilitato:", name);
589
- el.dataset.ualStatus = "disabled";
590
- collapse(el, slotCfg);
889
+ el2.dataset.ualStatus = "disabled";
890
+ collapse(el2, slotCfg);
591
891
  return;
592
892
  }
593
893
  let params = {};
594
894
  try {
595
- params = el.dataset.adParams ? JSON.parse(el.dataset.adParams) : {};
895
+ params = el2.dataset.adParams ? JSON.parse(el2.dataset.adParams) : {};
596
896
  } catch (e) {
597
- warn("data-ad-params non \xE8 JSON valido", el);
897
+ warn("data-ad-params non \xE8 JSON valido", el2);
598
898
  }
599
899
  const slot = {
600
- instanceId: el.dataset.ualId,
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(el);
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) el.style.minHeight = c.minHeight + "px";
620
- el.classList.add("ual-slot");
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
- el.appendChild(l);
926
+ el2.appendChild(l);
627
927
  }
628
928
  const box = document.createElement("div");
629
929
  box.className = "ual-creative";
630
- el.appendChild(box);
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 handle = renderInFrame(s.box, ad, {
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(el, c, force) {
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
- el.style.display = "none";
824
- el.style.minHeight = "";
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.1.1" : "dev";
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,