easter-egg-quest 1.0.16 → 1.0.17

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.
@@ -520,7 +520,25 @@ function isElementVisible(el) {
520
520
  if (rect.width === 0 || rect.height === 0) return false;
521
521
  const style = getComputedStyle(el);
522
522
  if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
523
- return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
523
+ const inViewport = rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
524
+ if (!inViewport) return false;
525
+ let parent = el.parentElement;
526
+ while (parent && parent !== document.body) {
527
+ const parentStyle = getComputedStyle(parent);
528
+ const overflowClips = [
529
+ parentStyle.overflow,
530
+ parentStyle.overflowX,
531
+ parentStyle.overflowY
532
+ ].some((value) => ["auto", "scroll", "hidden", "clip"].includes(value));
533
+ if (overflowClips) {
534
+ const parentRect = parent.getBoundingClientRect();
535
+ if (rect.bottom <= parentRect.top || rect.top >= parentRect.bottom || rect.right <= parentRect.left || rect.left >= parentRect.right) {
536
+ return false;
537
+ }
538
+ }
539
+ parent = parent.parentElement;
540
+ }
541
+ return true;
524
542
  }
525
543
  function isDangerousElement(el) {
526
544
  const text = (el.textContent ?? "").toLowerCase().trim();
@@ -528,26 +546,6 @@ function isDangerousElement(el) {
528
546
  const combined = `${text} ${ariaLabel}`;
529
547
  return DANGEROUS_KEYWORDS.some((kw) => combined.includes(kw));
530
548
  }
531
- function findEligibleEntryElements(containerSelector, excludeSelectors = []) {
532
- const container = containerSelector ? document.querySelector(containerSelector) ?? document.body : document.body;
533
- const candidates = container.querySelectorAll(
534
- 'a, button, [role="button"], nav a, nav button, .cta, [data-easter-entry]'
535
- );
536
- const excludeSet = /* @__PURE__ */ new Set();
537
- for (const sel of excludeSelectors) {
538
- document.querySelectorAll(sel).forEach((el) => excludeSet.add(el));
539
- }
540
- const eligible = [];
541
- candidates.forEach((el) => {
542
- if (excludeSet.has(el)) return;
543
- if (!isElementVisible(el)) return;
544
- if (isDangerousElement(el)) return;
545
- const rect = el.getBoundingClientRect();
546
- if (rect.width < 30 || rect.height < 20) return;
547
- eligible.push(el);
548
- });
549
- return eligible;
550
- }
551
549
  function pickRandom(arr) {
552
550
  if (arr.length === 0) return void 0;
553
551
  return arr[Math.floor(Math.random() * arr.length)];
@@ -577,6 +575,11 @@ class HiddenEntry {
577
575
  this._intersectionObserver = null;
578
576
  this._mutationDebounce = null;
579
577
  this._navigationHandler = null;
578
+ this._bootstrapObserver = null;
579
+ this._retryTimer = null;
580
+ this._fallbackTimer = null;
581
+ this._retryCount = 0;
582
+ this._historyPatched = false;
580
583
  this.config = config;
581
584
  this.script = script;
582
585
  this.onFound = onFound;
@@ -586,16 +589,10 @@ class HiddenEntry {
586
589
  this._createFallbackEntry();
587
590
  return;
588
591
  }
589
- const eligible = findEligibleEntryElements(
590
- this.config.hiddenEntry.selector,
591
- this.config.hiddenEntry.excludeSelectors
592
- );
593
- if (eligible.length === 0) {
594
- this._createFallbackEntry();
595
- return;
596
- }
597
- this._injectIntoExisting(eligible);
598
592
  this._startHintEscalation();
593
+ this._startBootstrapWatch();
594
+ this._scheduleFallback();
595
+ this._attemptInjection(0);
599
596
  }
600
597
  cleanup() {
601
598
  var _a2, _b2, _c, _d;
@@ -611,6 +608,7 @@ class HiddenEntry {
611
608
  this.targetElement.classList.remove("eeq-entry-target");
612
609
  }
613
610
  this._stopWatchdog();
611
+ this._stopBootstrapWatch();
614
612
  (_a2 = this._injectedElement) == null ? void 0 : _a2.remove();
615
613
  this._injectedElement = null;
616
614
  (_b2 = this.hintContainer) == null ? void 0 : _b2.remove();
@@ -623,17 +621,11 @@ class HiddenEntry {
623
621
  this._attachToElement(this.targetElement);
624
622
  }
625
623
  // ─── Inject trigger text inside an existing element ─────────────────
626
- _injectIntoExisting(_eligible) {
627
- const candidates = Array.from(
628
- document.querySelectorAll(
629
- "p, li, figcaption, blockquote, h4, h3, h2, h1, header a, nav a, nav button"
630
- )
631
- ).filter((el) => {
632
- var _a2;
633
- const text = ((_a2 = el.textContent) == null ? void 0 : _a2.trim()) ?? "";
634
- if (text.length < 2 || el.closest("[data-eeq]")) return false;
635
- return isElementVisible(el);
636
- });
624
+ _injectIntoExisting() {
625
+ var _a2;
626
+ const root = this._getSearchRoot();
627
+ const candidates = this._findTextCandidates(root);
628
+ if (candidates.length === 0) return false;
637
629
  const sorted = candidates.sort((a, b) => {
638
630
  const aNav = a.closest("nav") !== null || a.closest("header") !== null ? 1 : 0;
639
631
  const bNav = b.closest("nav") !== null || b.closest("header") !== null ? 1 : 0;
@@ -644,8 +636,8 @@ class HiddenEntry {
644
636
  });
645
637
  const prints = sorted.map(
646
638
  (el) => {
647
- var _a2;
648
- return `${el.tagName}:${(((_a2 = el.textContent) == null ? void 0 : _a2.trim()) ?? "").slice(0, 30)}`;
639
+ var _a3;
640
+ return `${el.tagName}:${(((_a3 = el.textContent) == null ? void 0 : _a3.trim()) ?? "").slice(0, 30)}`;
649
641
  }
650
642
  );
651
643
  const HISTORY_KEY = "eeq_trigger_history";
@@ -677,8 +669,8 @@ class HiddenEntry {
677
669
  ];
678
670
  const rotatedPrints = rotated.map(
679
671
  (el) => {
680
- var _a2;
681
- return `${el.tagName}:${(((_a2 = el.textContent) == null ? void 0 : _a2.trim()) ?? "").slice(0, 30)}`;
672
+ var _a3;
673
+ return `${el.tagName}:${(((_a3 = el.textContent) == null ? void 0 : _a3.trim()) ?? "").slice(0, 30)}`;
682
674
  }
683
675
  );
684
676
  const unused = rotated.filter((_, i) => !usedSet.has(rotatedPrints[i]));
@@ -692,6 +684,7 @@ class HiddenEntry {
692
684
  const isInline = host.tagName === "A" || host.tagName === "BUTTON" || host.tagName === "LI" || host.closest("nav") !== null;
693
685
  const span = document.createElement("span");
694
686
  span.textContent = isInline ? " · start hunt" : " start hunt";
687
+ span.setAttribute("data-eeq", "trigger");
695
688
  span.style.cssText = `
696
689
  font: inherit;
697
690
  color: inherit;
@@ -719,10 +712,161 @@ class HiddenEntry {
719
712
  this._injectedElement = span;
720
713
  this.targetElement = span;
721
714
  this._attachToElement(span);
715
+ this._retryCount = 0;
716
+ this._clearRetryTimer();
717
+ this._clearFallbackTimer();
718
+ (_a2 = this.fallbackButton) == null ? void 0 : _a2.remove();
719
+ this.fallbackButton = null;
720
+ this._stopBootstrapWatch();
722
721
  this._startWatchdog();
723
- return;
722
+ return true;
723
+ }
724
+ return false;
725
+ }
726
+ _attemptInjection(delayMs) {
727
+ this._clearRetryTimer();
728
+ this._retryTimer = setTimeout(() => {
729
+ if (this._destroyed || this._injectedElement) return;
730
+ const ok = this._injectIntoExisting();
731
+ if (!ok) this._scheduleNextRetry();
732
+ }, delayMs);
733
+ }
734
+ _scheduleNextRetry() {
735
+ if (this._destroyed || this._injectedElement) return;
736
+ const delays = [300, 900, 1800, 3200, 5e3, 8e3];
737
+ const nextDelay = delays[Math.min(this._retryCount, delays.length - 1)];
738
+ this._retryCount += 1;
739
+ this._attemptInjection(nextDelay);
740
+ }
741
+ _scheduleFallback() {
742
+ if (this.fallbackButton || this._fallbackTimer) return;
743
+ this._fallbackTimer = setTimeout(() => {
744
+ if (this._destroyed || this._injectedElement || this.fallbackButton) return;
745
+ this._createFallbackEntry();
746
+ }, 1e4);
747
+ }
748
+ _clearRetryTimer() {
749
+ if (this._retryTimer) {
750
+ clearTimeout(this._retryTimer);
751
+ this._retryTimer = null;
752
+ }
753
+ }
754
+ _clearFallbackTimer() {
755
+ if (this._fallbackTimer) {
756
+ clearTimeout(this._fallbackTimer);
757
+ this._fallbackTimer = null;
758
+ }
759
+ }
760
+ _getSearchRoot() {
761
+ if (!this.config.hiddenEntry.selector) return document.body;
762
+ return document.querySelector(this.config.hiddenEntry.selector) ?? document.body;
763
+ }
764
+ _findTextCandidates(root) {
765
+ var _a2;
766
+ const selector = [
767
+ "p",
768
+ "li",
769
+ "figcaption",
770
+ "blockquote",
771
+ "caption",
772
+ "label",
773
+ "td",
774
+ "th",
775
+ "h1",
776
+ "h2",
777
+ "h3",
778
+ "h4",
779
+ "h5",
780
+ "h6",
781
+ "a",
782
+ "button",
783
+ '[role="button"]',
784
+ '[role="link"]',
785
+ "div",
786
+ "span",
787
+ "article",
788
+ "section",
789
+ "summary",
790
+ "[data-easter-entry]",
791
+ "[mat-button]",
792
+ "[mat-list-item]",
793
+ ".mat-mdc-button",
794
+ ".mat-mdc-list-item",
795
+ ".ag-cell",
796
+ ".ag-header-cell"
797
+ ].join(", ");
798
+ const excludeSet = /* @__PURE__ */ new Set();
799
+ for (const sel of this.config.hiddenEntry.excludeSelectors) {
800
+ (_a2 = root.querySelectorAll) == null ? void 0 : _a2.call(root, sel).forEach((el) => excludeSet.add(el));
801
+ }
802
+ return Array.from(root.querySelectorAll(selector)).filter((el) => {
803
+ var _a3, _b2;
804
+ if (!(el instanceof HTMLElement)) return false;
805
+ if (excludeSet.has(el)) return false;
806
+ if (el.closest("[data-eeq]")) return false;
807
+ if (el.closest("script, style, noscript, svg, canvas, form")) return false;
808
+ if (isDangerousElement(el)) return false;
809
+ if (!isElementVisible(el)) return false;
810
+ const text = ((_a3 = el.innerText) == null ? void 0 : _a3.trim()) || ((_b2 = el.textContent) == null ? void 0 : _b2.trim()) || "";
811
+ if (text.length < 6 || text.length > 220) return false;
812
+ const childElementCount = el.children.length;
813
+ if (childElementCount > 12) return false;
814
+ const rect = el.getBoundingClientRect();
815
+ if (rect.width < 18 || rect.height < 12) return false;
816
+ return true;
817
+ });
818
+ }
819
+ _startBootstrapWatch() {
820
+ this._stopBootstrapWatch();
821
+ const root = this._getSearchRoot();
822
+ const observerTarget = root instanceof HTMLElement ? root : document.body;
823
+ this._bootstrapObserver = new MutationObserver(() => {
824
+ if (this._destroyed || this._injectedElement) return;
825
+ if (this._mutationDebounce) clearTimeout(this._mutationDebounce);
826
+ this._mutationDebounce = setTimeout(() => {
827
+ if (this._destroyed || this._injectedElement) return;
828
+ this._attemptInjection(150);
829
+ }, 250);
830
+ });
831
+ this._bootstrapObserver.observe(observerTarget, { childList: true, subtree: true });
832
+ this._navigationHandler = () => {
833
+ if (this._destroyed || this._injectedElement) return;
834
+ this._attemptInjection(700);
835
+ };
836
+ window.addEventListener("popstate", this._navigationHandler);
837
+ window.addEventListener("hashchange", this._navigationHandler);
838
+ if (!this._historyPatched && typeof window.history !== "undefined") {
839
+ this._historyPatched = true;
840
+ const patchHistoryMethod = (method) => {
841
+ const original = window.history[method].bind(window.history);
842
+ window.history[method] = (...args) => {
843
+ const result = original(...args);
844
+ window.dispatchEvent(new Event("eeq:navigation"));
845
+ return result;
846
+ };
847
+ };
848
+ patchHistoryMethod("pushState");
849
+ patchHistoryMethod("replaceState");
850
+ }
851
+ window.addEventListener("eeq:navigation", this._navigationHandler);
852
+ }
853
+ _stopBootstrapWatch() {
854
+ if (this._bootstrapObserver) {
855
+ this._bootstrapObserver.disconnect();
856
+ this._bootstrapObserver = null;
857
+ }
858
+ this._clearRetryTimer();
859
+ this._clearFallbackTimer();
860
+ if (this._mutationDebounce) {
861
+ clearTimeout(this._mutationDebounce);
862
+ this._mutationDebounce = null;
863
+ }
864
+ if (this._navigationHandler) {
865
+ window.removeEventListener("popstate", this._navigationHandler);
866
+ window.removeEventListener("hashchange", this._navigationHandler);
867
+ window.removeEventListener("eeq:navigation", this._navigationHandler);
868
+ this._navigationHandler = null;
724
869
  }
725
- this._createFallbackEntry();
726
870
  }
727
871
  /**
728
872
  * Watch for the injected trigger element being removed from the DOM
@@ -769,6 +913,7 @@ class HiddenEntry {
769
913
  };
770
914
  window.addEventListener("popstate", this._navigationHandler);
771
915
  window.addEventListener("hashchange", this._navigationHandler);
916
+ window.addEventListener("eeq:navigation", this._navigationHandler);
772
917
  }
773
918
  /**
774
919
  * Walk up the DOM to find a stable ancestor that won't be replaced
@@ -803,6 +948,7 @@ class HiddenEntry {
803
948
  if (this._navigationHandler) {
804
949
  window.removeEventListener("popstate", this._navigationHandler);
805
950
  window.removeEventListener("hashchange", this._navigationHandler);
951
+ window.removeEventListener("eeq:navigation", this._navigationHandler);
806
952
  this._navigationHandler = null;
807
953
  }
808
954
  }
@@ -817,13 +963,12 @@ class HiddenEntry {
817
963
  }
818
964
  this._injectedElement = null;
819
965
  this.targetElement = null;
966
+ this._startBootstrapWatch();
967
+ this._scheduleFallback();
820
968
  setTimeout(() => {
821
969
  if (this._destroyed) return;
822
- const eligible = findEligibleEntryElements(
823
- this.config.hiddenEntry.selector,
824
- this.config.hiddenEntry.excludeSelectors
825
- );
826
- this._injectIntoExisting(eligible);
970
+ const ok = this._injectIntoExisting();
971
+ if (!ok) this._scheduleNextRetry();
827
972
  }, 1200);
828
973
  }
829
974
  /**
@@ -832,12 +977,22 @@ class HiddenEntry {
832
977
  * Returns true if inserted without layout shift.
833
978
  */
834
979
  _tryInsertBetweenWords(host, span) {
835
- var _a2;
836
980
  const textNodes = [];
837
- for (const node of host.childNodes) {
838
- if (node.nodeType === Node.TEXT_NODE && (((_a2 = node.textContent) == null ? void 0 : _a2.trim().length) ?? 0) > 5) {
839
- textNodes.push(node);
981
+ const walker = document.createTreeWalker(host, NodeFilter.SHOW_TEXT, {
982
+ acceptNode: (node) => {
983
+ var _a2;
984
+ const text2 = ((_a2 = node.textContent) == null ? void 0 : _a2.trim()) ?? "";
985
+ if (text2.length <= 5) return NodeFilter.FILTER_REJECT;
986
+ const parent = node.parentElement;
987
+ if (!parent) return NodeFilter.FILTER_REJECT;
988
+ if (parent.closest("[data-eeq], script, style, noscript")) return NodeFilter.FILTER_REJECT;
989
+ return NodeFilter.FILTER_ACCEPT;
840
990
  }
991
+ });
992
+ let current = walker.nextNode();
993
+ while (current) {
994
+ textNodes.push(current);
995
+ current = walker.nextNode();
841
996
  }
842
997
  if (!textNodes.length) return false;
843
998
  const slotSeed = Math.floor(Date.now() / (1e3 * 60 * 2));
@@ -854,13 +1009,13 @@ class HiddenEntry {
854
1009
  const splitAt = spacePositions[Math.min(idx, spacePositions.length - 1)];
855
1010
  const before = text.slice(0, splitAt);
856
1011
  const after = text.slice(splitAt);
857
- const heightBefore = host.getBoundingClientRect().height;
1012
+ const rectBefore = host.getBoundingClientRect();
858
1013
  const afterNode = document.createTextNode(after);
859
1014
  textNode.textContent = before;
860
1015
  host.insertBefore(afterNode, textNode.nextSibling);
861
1016
  host.insertBefore(span, afterNode);
862
- const heightAfter = host.getBoundingClientRect().height;
863
- if (Math.abs(heightAfter - heightBefore) > 2) {
1017
+ const rectAfter = host.getBoundingClientRect();
1018
+ if (Math.abs(rectAfter.height - rectBefore.height) > 8 || Math.abs(rectAfter.width - rectBefore.width) > 24) {
864
1019
  host.removeChild(span);
865
1020
  host.removeChild(afterNode);
866
1021
  textNode.textContent = text;
@@ -1105,6 +1260,7 @@ class HiddenEntry {
1105
1260
  document.head.appendChild(this.shimmerStyle);
1106
1261
  }
1107
1262
  _createFallbackEntry() {
1263
+ if (this.fallbackButton) return;
1108
1264
  this.fallbackButton = document.createElement("button");
1109
1265
  this.fallbackButton.textContent = "·";
1110
1266
  this.fallbackButton.setAttribute("aria-label", "Hidden game entry");