@seatlayer/js 0.22.0 → 0.24.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/README.md +54 -3
- package/dist/index.cjs +204 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -10
- package/dist/index.d.ts +117 -10
- package/dist/index.js +204 -20
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -172,8 +172,28 @@ var SeatingChart = class {
|
|
|
172
172
|
ribbon.style.cssText = "position:absolute;top:18px;right:-34px;z-index:6;transform:rotate(45deg);width:140px;text-align:center;padding:4px 0;background:#f4b740;color:#1a1200;font:800 10.5px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;letter-spacing:.12em;box-shadow:0 2px 8px rgba(0,0,0,.25);pointer-events:none;";
|
|
173
173
|
host.appendChild(ribbon);
|
|
174
174
|
}
|
|
175
|
+
this.buildBadge(host);
|
|
175
176
|
return this;
|
|
176
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Attribution badge pinned to the embed's bottom-right, linking to
|
|
180
|
+
* seatlayer.io. Rendered as an absolutely-positioned overlay with
|
|
181
|
+
* self-contained inline styles — the SDK embed ships no widget CSS, and an
|
|
182
|
+
* overlay keeps it out of the layout flow so it never disturbs the SDK v0.22
|
|
183
|
+
* fill-height resize contract. Mirrors the full widget's mark + wordmark and
|
|
184
|
+
* reuses the `picker.poweredBy` i18n string.
|
|
185
|
+
*/
|
|
186
|
+
buildBadge(host) {
|
|
187
|
+
if (this.controller.doc?.theme?.hideBadge) return;
|
|
188
|
+
const badge = document.createElement("a");
|
|
189
|
+
badge.href = "https://seatlayer.io";
|
|
190
|
+
badge.target = "_blank";
|
|
191
|
+
badge.rel = "noopener noreferrer";
|
|
192
|
+
badge.setAttribute("aria-label", t("picker.poweredBy"));
|
|
193
|
+
badge.style.cssText = 'position:absolute;bottom:10px;right:12px;z-index:5;display:inline-flex;align-items:center;gap:6px;padding:5px 9px;border-radius:999px;background:rgba(255,255,255,.92);color:#4a5163;text-decoration:none;font:600 11px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;letter-spacing:.02em;box-shadow:0 2px 8px rgba(0,0,0,.12);';
|
|
194
|
+
badge.innerHTML = `<span aria-hidden="true" style="width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;background:#f4b740;color:#1a1200"><svg viewBox="0 0 24 24" style="width:11px;height:11px;fill:currentColor"><path d="M4 15c0-1.1.9-2 2-2h12a2 2 0 0 1 2 2v3h-3v-2H7v2H4v-3Z"/><rect x="7" y="7" width="10" height="5" rx="1.6"/></svg></span><span>${t("picker.poweredBy")}</span>`;
|
|
195
|
+
host.appendChild(badge);
|
|
196
|
+
}
|
|
177
197
|
placeTooltip() {
|
|
178
198
|
if (!this.tipEl || !this.hostEl) return;
|
|
179
199
|
const hw = this.hostEl.clientWidth;
|
|
@@ -328,6 +348,13 @@ var TYPES = /* @__PURE__ */ new Set([
|
|
|
328
348
|
]);
|
|
329
349
|
var DEFAULT_LOADING_TIMEOUT_MS = 2e4;
|
|
330
350
|
var DEFAULT_MIN_FILL_HEIGHT = 480;
|
|
351
|
+
var RENEW_LEAD_MS = 3 * 60 * 1e3;
|
|
352
|
+
var RENEW_SHORT_TTL_MS = 15 * 60 * 1e3;
|
|
353
|
+
var RENEW_SHORT_TTL_FRACTION = 0.8;
|
|
354
|
+
var RENEW_MIN_DELAY_MS = 30 * 1e3;
|
|
355
|
+
var FILL_PROBE_HEIGHT_PX = 1e5;
|
|
356
|
+
var FILL_PROBE_TRACK_EPSILON_PX = 4;
|
|
357
|
+
var FILL_MIN_DEFINITE_HEIGHT_PX = 50;
|
|
331
358
|
function resolveContainer2(container) {
|
|
332
359
|
if (typeof container !== "string") return container;
|
|
333
360
|
const element = document.querySelector(container);
|
|
@@ -365,6 +392,14 @@ var EmbeddedDesigner = class {
|
|
|
365
392
|
this.designerOrigin = "";
|
|
366
393
|
this.overlay = null;
|
|
367
394
|
this.timeoutTimer = null;
|
|
395
|
+
/** Proactive session-renewal timer; armed from each `ready`, cleared on re-mount. */
|
|
396
|
+
this.renewTimer = null;
|
|
397
|
+
/**
|
|
398
|
+
* One automatic recovery relaunch is allowed per expiry. Reset ONLY when a fresh
|
|
399
|
+
* `ready` arrives — deliberately not on re-mount — so a session that keeps failing
|
|
400
|
+
* to load can't loop the host through endless silent relaunches.
|
|
401
|
+
*/
|
|
402
|
+
this.autoRecoverUsed = false;
|
|
368
403
|
this.phase = "loading";
|
|
369
404
|
this.restoreContainerPosition = null;
|
|
370
405
|
// Host-side fullscreen pin: saved state we restore on `off`/Escape/destroy.
|
|
@@ -375,10 +410,17 @@ var EmbeddedDesigner = class {
|
|
|
375
410
|
this.fsKeyHandler = null;
|
|
376
411
|
/** Latest height (px string) the Designer reported; re-applied after unpin. */
|
|
377
412
|
this.lastAutoHeight = "";
|
|
378
|
-
//
|
|
413
|
+
// Fill sizing: pending rAF handles + whether window listeners are attached.
|
|
379
414
|
this.fillRaf = null;
|
|
415
|
+
this.reprobeRaf = null;
|
|
380
416
|
this.fillListening = false;
|
|
381
|
-
/**
|
|
417
|
+
/** Resolved container element (fill measurement + ResizeObserver target). */
|
|
418
|
+
this.containerEl = null;
|
|
419
|
+
/** Cached fill verdict: 'container' = bounded block, 'viewport' = full page. */
|
|
420
|
+
this.fillMode = null;
|
|
421
|
+
/** Live block-size tracking in container-fill mode; disconnected on destroy. */
|
|
422
|
+
this.resizeObs = null;
|
|
423
|
+
/** rAF-throttled fill recompute, so a burst of scroll/RO ticks coalesces. */
|
|
382
424
|
this.scheduleFill = () => {
|
|
383
425
|
if (this.fillRaf !== null) return;
|
|
384
426
|
this.fillRaf = requestAnimationFrame(() => {
|
|
@@ -386,6 +428,21 @@ var EmbeddedDesigner = class {
|
|
|
386
428
|
this.applyFill();
|
|
387
429
|
});
|
|
388
430
|
};
|
|
431
|
+
/**
|
|
432
|
+
* rAF-throttled re-probe: a host layout change (responsive breakpoint, a block
|
|
433
|
+
* gaining/losing a definite height) can flip the verdict, so `resize` /
|
|
434
|
+
* `orientationchange` re-detect and swap the container observer accordingly.
|
|
435
|
+
*/
|
|
436
|
+
this.scheduleReprobe = () => {
|
|
437
|
+
if (this.reprobeRaf !== null) return;
|
|
438
|
+
this.reprobeRaf = requestAnimationFrame(() => {
|
|
439
|
+
this.reprobeRaf = null;
|
|
440
|
+
if (this.pinned) return;
|
|
441
|
+
this.fillMode = this.detectFillMode();
|
|
442
|
+
this.syncContainerObserver();
|
|
443
|
+
this.applyFill();
|
|
444
|
+
});
|
|
445
|
+
};
|
|
389
446
|
this.handleMessage = (event) => {
|
|
390
447
|
if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
|
|
391
448
|
if (!event.data || typeof event.data !== "object") return;
|
|
@@ -393,7 +450,7 @@ var EmbeddedDesigner = class {
|
|
|
393
450
|
if (data.type === "seatlayer.designer.resize") {
|
|
394
451
|
if (!this.fillEnabled() && this.autoResizeEnabled() && typeof data.px === "number" && Number.isFinite(data.px) && data.px > 0) {
|
|
395
452
|
this.lastAutoHeight = `${Math.round(data.px)}px`;
|
|
396
|
-
if (!this.pinned) this.
|
|
453
|
+
if (!this.pinned) this.setFrameHeight(this.lastAutoHeight);
|
|
397
454
|
}
|
|
398
455
|
return;
|
|
399
456
|
}
|
|
@@ -421,6 +478,8 @@ var EmbeddedDesigner = class {
|
|
|
421
478
|
this.phase = "ready";
|
|
422
479
|
this.clearTimeoutTimer();
|
|
423
480
|
this.removeOverlay();
|
|
481
|
+
this.autoRecoverUsed = false;
|
|
482
|
+
this.scheduleRenewal(message.expiresAt);
|
|
424
483
|
this.options.onReady?.(message);
|
|
425
484
|
break;
|
|
426
485
|
case "seatlayer.designer.saved":
|
|
@@ -432,10 +491,18 @@ var EmbeddedDesigner = class {
|
|
|
432
491
|
case "seatlayer.designer.close":
|
|
433
492
|
this.options.onClose?.(message);
|
|
434
493
|
break;
|
|
435
|
-
case "seatlayer.designer.error":
|
|
436
|
-
|
|
494
|
+
case "seatlayer.designer.error": {
|
|
495
|
+
const cause = causeFromCode(message.code);
|
|
496
|
+
if (cause === "expired" && this.autoRenewEnabled() && !this.autoRecoverUsed) {
|
|
497
|
+
this.autoRecoverUsed = true;
|
|
498
|
+
this.clearRenewTimer();
|
|
499
|
+
this.options.onRequestRelaunch();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
this.showError(cause);
|
|
437
503
|
this.options.onError?.(message);
|
|
438
504
|
break;
|
|
505
|
+
}
|
|
439
506
|
}
|
|
440
507
|
};
|
|
441
508
|
this.options = options;
|
|
@@ -452,12 +519,17 @@ var EmbeddedDesigner = class {
|
|
|
452
519
|
frame.allow = this.options.allow ?? "fullscreen; clipboard-write";
|
|
453
520
|
frame.referrerPolicy = this.options.referrerPolicy ?? "origin";
|
|
454
521
|
frame.src = url.toString();
|
|
455
|
-
frame.style.width
|
|
456
|
-
frame.style.
|
|
522
|
+
frame.style.setProperty("width", "100%", "important");
|
|
523
|
+
frame.style.setProperty(
|
|
524
|
+
"height",
|
|
525
|
+
typeof this.options.height === "number" ? `${this.options.height}px` : "100%",
|
|
526
|
+
"important"
|
|
527
|
+
);
|
|
457
528
|
frame.style.border = "0";
|
|
458
529
|
Object.assign(frame.style, this.options.style);
|
|
459
530
|
if (this.options.className) frame.className = this.options.className;
|
|
460
531
|
const container = resolveContainer2(this.options.container);
|
|
532
|
+
this.containerEl = container;
|
|
461
533
|
window.addEventListener("message", this.handleMessage);
|
|
462
534
|
container.append(frame);
|
|
463
535
|
this.frame = frame;
|
|
@@ -488,10 +560,13 @@ var EmbeddedDesigner = class {
|
|
|
488
560
|
this.stopFill();
|
|
489
561
|
this.unpinFullscreen();
|
|
490
562
|
this.clearTimeoutTimer();
|
|
563
|
+
this.clearRenewTimer();
|
|
491
564
|
this.removeOverlay();
|
|
492
565
|
this.restoreContainerStyle();
|
|
493
566
|
this.frame?.remove();
|
|
494
567
|
this.frame = null;
|
|
568
|
+
this.containerEl = null;
|
|
569
|
+
this.fillMode = null;
|
|
495
570
|
this.designerOrigin = "";
|
|
496
571
|
this.phase = "loading";
|
|
497
572
|
this.lastAutoHeight = "";
|
|
@@ -506,24 +581,77 @@ var EmbeddedDesigner = class {
|
|
|
506
581
|
fillEnabled() {
|
|
507
582
|
return typeof this.options.height !== "number";
|
|
508
583
|
}
|
|
584
|
+
/** Write an SDK-managed height with `!important` so a host theme can't win. */
|
|
585
|
+
setFrameHeight(value) {
|
|
586
|
+
this.frame?.style.setProperty("height", value, "important");
|
|
587
|
+
}
|
|
509
588
|
/**
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
589
|
+
* Decide whether the host gave the container a DEFINITE (bounded) height — a
|
|
590
|
+
* fixed block the embed should fill 100% of — versus a content-sized container
|
|
591
|
+
* that collapses to whatever the iframe measures (full-page usage).
|
|
592
|
+
*
|
|
593
|
+
* We drive the iframe to two extreme heights within a single synchronous task
|
|
594
|
+
* and watch whether the container follows: a bounded box barely moves, a
|
|
595
|
+
* content-sized one grows with the iframe. Because we restore the height before
|
|
596
|
+
* yielding, the browser only lays out — it never paints the extremes, so there
|
|
597
|
+
* is no visible flash. Works for px, resolved `%`, and flex (`flex:1;min-h:0`)
|
|
598
|
+
* heights, and leaves a mere `min-height` floor classified as content-sized so
|
|
599
|
+
* full-page hosts keep the old viewport-fill behavior.
|
|
600
|
+
*/
|
|
601
|
+
detectFillMode() {
|
|
602
|
+
const container = this.containerEl;
|
|
603
|
+
const frame = this.frame;
|
|
604
|
+
if (this.pinned || !container || !frame) return this.fillMode ?? "viewport";
|
|
605
|
+
const measure = () => container.getBoundingClientRect().height;
|
|
606
|
+
const savedValue = frame.style.getPropertyValue("height");
|
|
607
|
+
const savedPriority = frame.style.getPropertyPriority("height");
|
|
608
|
+
frame.style.setProperty("height", "0px", "important");
|
|
609
|
+
const collapsed = measure();
|
|
610
|
+
frame.style.setProperty("height", `${FILL_PROBE_HEIGHT_PX}px`, "important");
|
|
611
|
+
const expanded = measure();
|
|
612
|
+
if (savedValue) frame.style.setProperty("height", savedValue, savedPriority);
|
|
613
|
+
else frame.style.removeProperty("height");
|
|
614
|
+
const tracksIframe = expanded - collapsed > FILL_PROBE_TRACK_EPSILON_PX;
|
|
615
|
+
const bounded = !tracksIframe && collapsed >= FILL_MIN_DEFINITE_HEIGHT_PX;
|
|
616
|
+
return bounded ? "container" : "viewport";
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Size the iframe for the current fill verdict, clamped to `minHeight`. In
|
|
620
|
+
* container mode it fills 100% of the bounded block; in viewport mode its
|
|
621
|
+
* bottom edge meets the bottom of the viewport (`window.innerHeight - top`).
|
|
622
|
+
* No-op while pinned fullscreen (the pin fills the viewport itself).
|
|
513
623
|
*/
|
|
514
624
|
applyFill() {
|
|
515
625
|
if (!this.frame || this.pinned) return;
|
|
516
626
|
const min = this.options.minHeight ?? DEFAULT_MIN_FILL_HEIGHT;
|
|
627
|
+
if (this.fillMode === "container" && this.containerEl) {
|
|
628
|
+
const target2 = Math.max(min, Math.round(this.containerEl.getBoundingClientRect().height));
|
|
629
|
+
this.setFrameHeight(`${target2}px`);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
517
632
|
const top = this.frame.getBoundingClientRect().top;
|
|
518
633
|
const target = Math.max(min, Math.round(window.innerHeight - top));
|
|
519
|
-
this.
|
|
634
|
+
this.setFrameHeight(`${target}px`);
|
|
635
|
+
}
|
|
636
|
+
/** Attach/detach the container ResizeObserver to match the current verdict. */
|
|
637
|
+
syncContainerObserver() {
|
|
638
|
+
const want = this.fillMode === "container" && !!this.containerEl && typeof ResizeObserver !== "undefined";
|
|
639
|
+
if (want && !this.resizeObs) {
|
|
640
|
+
this.resizeObs = new ResizeObserver(() => this.scheduleFill());
|
|
641
|
+
this.resizeObs.observe(this.containerEl);
|
|
642
|
+
} else if (!want && this.resizeObs) {
|
|
643
|
+
this.resizeObs.disconnect();
|
|
644
|
+
this.resizeObs = null;
|
|
645
|
+
}
|
|
520
646
|
}
|
|
521
647
|
startFill() {
|
|
648
|
+
this.fillMode = this.detectFillMode();
|
|
649
|
+
this.syncContainerObserver();
|
|
522
650
|
this.applyFill();
|
|
523
651
|
if (this.fillListening) return;
|
|
524
652
|
this.fillListening = true;
|
|
525
|
-
window.addEventListener("resize", this.
|
|
526
|
-
window.addEventListener("orientationchange", this.
|
|
653
|
+
window.addEventListener("resize", this.scheduleReprobe);
|
|
654
|
+
window.addEventListener("orientationchange", this.scheduleReprobe);
|
|
527
655
|
window.addEventListener("scroll", this.scheduleFill, { passive: true });
|
|
528
656
|
}
|
|
529
657
|
stopFill() {
|
|
@@ -531,10 +659,18 @@ var EmbeddedDesigner = class {
|
|
|
531
659
|
cancelAnimationFrame(this.fillRaf);
|
|
532
660
|
this.fillRaf = null;
|
|
533
661
|
}
|
|
662
|
+
if (this.reprobeRaf !== null) {
|
|
663
|
+
cancelAnimationFrame(this.reprobeRaf);
|
|
664
|
+
this.reprobeRaf = null;
|
|
665
|
+
}
|
|
666
|
+
if (this.resizeObs) {
|
|
667
|
+
this.resizeObs.disconnect();
|
|
668
|
+
this.resizeObs = null;
|
|
669
|
+
}
|
|
534
670
|
if (!this.fillListening) return;
|
|
535
671
|
this.fillListening = false;
|
|
536
|
-
window.removeEventListener("resize", this.
|
|
537
|
-
window.removeEventListener("orientationchange", this.
|
|
672
|
+
window.removeEventListener("resize", this.scheduleReprobe);
|
|
673
|
+
window.removeEventListener("orientationchange", this.scheduleReprobe);
|
|
538
674
|
window.removeEventListener("scroll", this.scheduleFill);
|
|
539
675
|
}
|
|
540
676
|
/**
|
|
@@ -546,16 +682,22 @@ var EmbeddedDesigner = class {
|
|
|
546
682
|
if (this.pinned || !this.frame) return;
|
|
547
683
|
this.pinned = true;
|
|
548
684
|
this.frameStyleBeforeFs = this.frame.getAttribute("style");
|
|
549
|
-
|
|
685
|
+
const pin = {
|
|
550
686
|
position: "fixed",
|
|
551
|
-
|
|
687
|
+
top: "0",
|
|
688
|
+
right: "0",
|
|
689
|
+
bottom: "0",
|
|
690
|
+
left: "0",
|
|
552
691
|
width: "100vw",
|
|
553
692
|
height: "100vh",
|
|
554
693
|
margin: "0",
|
|
555
694
|
border: "0",
|
|
556
|
-
|
|
695
|
+
"z-index": "2147483000",
|
|
557
696
|
background: "#101625"
|
|
558
|
-
}
|
|
697
|
+
};
|
|
698
|
+
for (const [property, value] of Object.entries(pin)) {
|
|
699
|
+
this.frame.style.setProperty(property, value, "important");
|
|
700
|
+
}
|
|
559
701
|
const docEl = document.documentElement;
|
|
560
702
|
this.docOverflowBeforeFs = docEl.style.overflow;
|
|
561
703
|
docEl.style.overflow = "hidden";
|
|
@@ -576,7 +718,7 @@ var EmbeddedDesigner = class {
|
|
|
576
718
|
if (this.frameStyleBeforeFs === null) this.frame.removeAttribute("style");
|
|
577
719
|
else this.frame.setAttribute("style", this.frameStyleBeforeFs);
|
|
578
720
|
if (this.fillEnabled()) this.applyFill();
|
|
579
|
-
else if (this.autoResizeEnabled() && this.lastAutoHeight) this.
|
|
721
|
+
else if (this.autoResizeEnabled() && this.lastAutoHeight) this.setFrameHeight(this.lastAutoHeight);
|
|
580
722
|
}
|
|
581
723
|
this.frameStyleBeforeFs = null;
|
|
582
724
|
if (this.docOverflowBeforeFs !== null) {
|
|
@@ -598,6 +740,48 @@ var EmbeddedDesigner = class {
|
|
|
598
740
|
this.timeoutTimer = null;
|
|
599
741
|
}
|
|
600
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* Auto-renewal (proactive + one expiry recovery) is on when the host wired a
|
|
745
|
+
* relaunch hook and did not opt out. Without the hook there is nothing to call,
|
|
746
|
+
* so it is a no-op.
|
|
747
|
+
*/
|
|
748
|
+
autoRenewEnabled() {
|
|
749
|
+
return !!this.options.onRequestRelaunch && this.options.autoRenewSession !== false;
|
|
750
|
+
}
|
|
751
|
+
clearRenewTimer() {
|
|
752
|
+
if (this.renewTimer !== null) {
|
|
753
|
+
clearTimeout(this.renewTimer);
|
|
754
|
+
this.renewTimer = null;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Arm the proactive renewal timer from a `ready` message's `expiresAt` (epoch
|
|
759
|
+
* ms). We relaunch a comfortable lead before expiry so the host can mint a fresh
|
|
760
|
+
* session and swap `designerUrl` without the user ever seeing the expiry card:
|
|
761
|
+
*
|
|
762
|
+
* - normal TTL (≥ 15 min): renew {@link RENEW_LEAD_MS} (~3 min) before expiry;
|
|
763
|
+
* - short TTL (< 15 min): renew after {@link RENEW_SHORT_TTL_FRACTION} (80%) of
|
|
764
|
+
* the remaining life, so the lead can't overshoot the whole session;
|
|
765
|
+
* - either way, never sooner than {@link RENEW_MIN_DELAY_MS} (30s) after `ready`
|
|
766
|
+
* so a burst of `ready` messages can't spin the host.
|
|
767
|
+
*
|
|
768
|
+
* Re-armed on every `ready`; cleared on destroy / setDesignerUrl (via re-mount).
|
|
769
|
+
* A no-op when auto-renewal is off or `expiresAt` is missing/already past — the
|
|
770
|
+
* expiry-error path recovers a session that has already lapsed.
|
|
771
|
+
*/
|
|
772
|
+
scheduleRenewal(expiresAt) {
|
|
773
|
+
this.clearRenewTimer();
|
|
774
|
+
if (!this.autoRenewEnabled()) return;
|
|
775
|
+
if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return;
|
|
776
|
+
const remaining = expiresAt - Date.now();
|
|
777
|
+
if (remaining <= 0) return;
|
|
778
|
+
const lead = remaining < RENEW_SHORT_TTL_MS ? remaining * RENEW_SHORT_TTL_FRACTION : remaining - RENEW_LEAD_MS;
|
|
779
|
+
const delay = Math.max(RENEW_MIN_DELAY_MS, lead);
|
|
780
|
+
this.renewTimer = setTimeout(() => {
|
|
781
|
+
this.renewTimer = null;
|
|
782
|
+
if (this.autoRenewEnabled()) this.options.onRequestRelaunch();
|
|
783
|
+
}, delay);
|
|
784
|
+
}
|
|
601
785
|
ensureContainerPositioned(container) {
|
|
602
786
|
const position = getComputedStyle(container).position;
|
|
603
787
|
if (position === "static") {
|