diff --git a/README.md b/README.md
index cb115012c..464cec393 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,7 @@ The project uses several custom `$settings` in `settings.php`:
| `telegram_token` | Telegram Bot API token for comment moderation |
| `telegram_secret_token` | Secret token for Telegram webhook verification |
| `telegram_chat_id` | Telegram chat ID for moderation notifications |
+| `app_foresight` | (default: `TRUE`) Allows to disable the ForesightJS prefetch library for an environment. Only active for anonymous users. When disabled, no link prefetching is performed. |
## ๐งฌ Quality Tools
diff --git a/app/modules/platform/app_platform.libraries.yml b/app/modules/platform/app_platform.libraries.yml
new file mode 100644
index 000000000..4d66dd879
--- /dev/null
+++ b/app/modules/platform/app_platform.libraries.yml
@@ -0,0 +1,10 @@
+foresightjs:
+ js:
+ /libraries/foresight/dist/foresight.umd.js: { weight: -20, minified: true, preprocess: false }
+
+foresightjs.init:
+ js:
+ assets/js/foresight.init.js: { weight: -10 }
+ dependencies:
+ - core/drupal
+ - app_platform/foresightjs
diff --git a/app/modules/platform/assets/js/foresight.init.js b/app/modules/platform/assets/js/foresight.init.js
new file mode 100644
index 000000000..d6030ab8f
--- /dev/null
+++ b/app/modules/platform/assets/js/foresight.init.js
@@ -0,0 +1,80 @@
+((Drupal, ForesightLib) => {
+
+ const { origin } = window.location;
+ const prefetched = new Set();
+
+ const IGNORE_URL_PATTERNS = [
+ '/user/logout',
+ '/admin',
+ '/edit',
+ '/ajax',
+ '/api',
+ ];
+
+ const IGNORE_CONTAINER_SELECTORS = [
+ '#block-local-tasks-block a',
+ '.block-local-tasks-block a',
+ '#drupal-off-canvas a',
+ '#toolbar-administration a',
+ ];
+
+ function prefetch(url) {
+ if (prefetched.has(url)) {
+ return;
+ }
+ prefetched.add(url);
+ const link = document.createElement('link');
+ link.rel = 'prefetch';
+ link.href = url;
+ document.head.appendChild(link);
+ }
+
+ function shouldSkip(anchor) {
+ const { href } = anchor;
+ if (!href || !href.startsWith(origin)) {
+ return true;
+ }
+ if (anchor.hasAttribute('download') || anchor.hasAttribute('noprefetch')) {
+ return true;
+ }
+ if (anchor.classList.contains('use-ajax')) {
+ return true;
+ }
+ if (IGNORE_URL_PATTERNS.some((pattern) => href.includes(pattern))) {
+ return true;
+ }
+ if (IGNORE_CONTAINER_SELECTORS.some((selector) => anchor.matches(selector))) {
+ return true;
+ }
+ // Skip links to files (e.g. .pdf, .zip).
+ if (/\.[^/?#]{1,5}([?#]|$)/.test(new URL(href).pathname)) {
+ return true;
+ }
+ return false;
+ }
+
+ Drupal.behaviors.foresight = {
+ attach(context) {
+ if (!ForesightLib) {
+ return;
+ }
+
+ const { ForesightManager } = ForesightLib;
+
+ if (!ForesightManager.instance) {
+ ForesightManager.initialize();
+ }
+
+ context.querySelectorAll('a[href]').forEach((anchor) => {
+ if (shouldSkip(anchor)) {
+ return;
+ }
+ ForesightManager.instance.register({
+ element: anchor,
+ callback: () => prefetch(anchor.href),
+ });
+ });
+ },
+ };
+
+})(Drupal, window.ForesightLib);
diff --git a/app/modules/platform/composer.json b/app/modules/platform/composer.json
index 822d9018e..31f44391d 100644
--- a/app/modules/platform/composer.json
+++ b/app/modules/platform/composer.json
@@ -7,6 +7,7 @@
],
"require": {
"app/contract": "^1.0",
+ "app-asset/foresight": "^3.5",
"app-asset/photoswipe": "^5.4",
"league/html-to-markdown": "^5.1"
}
diff --git a/app/modules/platform/src/AppPlatformServiceProvider.php b/app/modules/platform/src/AppPlatformServiceProvider.php
index fb8619da3..dbccf9345 100644
--- a/app/modules/platform/src/AppPlatformServiceProvider.php
+++ b/app/modules/platform/src/AppPlatformServiceProvider.php
@@ -11,6 +11,7 @@
use Drupal\app_platform\Hook\Asset\CacheBustingQuerySetting;
use Drupal\app_platform\Hook\Core\LlmsPageAttachments;
use Drupal\app_platform\Hook\Theme\LibraryInfoAlter;
+use Drupal\app_platform\Http\DateHeaderMiddleware;
use Drupal\app_platform\LanguageAwareStore\EventSubscriber\LanguageAwareSettingsRoutes;
use Drupal\app_platform\LanguageAwareStore\Factory\DatabaseLanguageAwareFactory;
use Drupal\app_platform\LanguageAwareStore\Factory\ServiceContainerLanguageAwareFactory;
@@ -38,6 +39,10 @@ public function register(ContainerBuilder $container): void {
->setPublic(TRUE)
->setAutoconfigured(TRUE);
+ $container->register('app_platform.date_header_middleware', DateHeaderMiddleware::class)
+ ->setPublic(TRUE)
+ ->addTag('http_middleware', ['priority' => 201]);
+
// Console.
$container
->autowire('app_platform.process.terminal', ProcessTerminal::class)
diff --git a/app/modules/platform/src/Hook/ForesightPageAttachments.php b/app/modules/platform/src/Hook/ForesightPageAttachments.php
new file mode 100644
index 000000000..8327dad0a
--- /dev/null
+++ b/app/modules/platform/src/Hook/ForesightPageAttachments.php
@@ -0,0 +1,35 @@
+requestStack->getCurrentRequest();
+ if ($request && $this->sessionConfiguration->hasSession($request)) {
+ return;
+ }
+
+ $attachments['#attached']['library'][] = 'app_platform/foresightjs.init';
+ }
+
+}
diff --git a/app/modules/platform/src/Http/DateHeaderMiddleware.php b/app/modules/platform/src/Http/DateHeaderMiddleware.php
new file mode 100644
index 000000000..ab1541b12
--- /dev/null
+++ b/app/modules/platform/src/Http/DateHeaderMiddleware.php
@@ -0,0 +1,74 @@
+ max-age=900 and treats the
+ * response as stale โ even though it just arrived. A stale response
+ * cannot be stored in the prefetch cache, so
+ * requests made by ForesightJS are discarded and the next navigation
+ * still hits the server.
+ *
+ * Why CDNs do not have this problem:
+ * CDNs such as Cloudflare replace Date with the current time before
+ * forwarding the response. Without a CDN the frozen date reaches the
+ * browser unchanged.
+ *
+ * RFC note:
+ * RFC 7231 ยง7.1.1.2 defines Date as "the date and time at which the
+ * message was originated". This is intentionally ambiguous for cached
+ * responses: it could mean when the content was first generated (Drupal
+ * freezes it here) or when the HTTP message is transmitted to the client
+ * (what this middleware sets it to). RFC 7234 ยง5.1 offers the formally
+ * correct solution โ adding an Age header so the browser knows how old
+ * the stored response is โ but with max-age=900 and a cache entry that
+ * lives indefinitely (invalidated by cache tags, not by TTL), Age would
+ * still make the response appear stale. Resetting Date to the current
+ * transmission time is the pragmatic fix, and matches what CDNs such as
+ * Cloudflare do in practice.
+ *
+ * Solution:
+ * This middleware wraps the kernel stack at priority 201 (just above
+ * PageCache at 200) and overwrites Date with the current time on every
+ * response. The browser always sees apparent_age โ 0 and the freshness
+ * window starts from the moment of receipt.
+ *
+ * Last-Modified and ETag are not touched: Last-Modified still reflects
+ * the actual content-change time and ETag still enables conditional
+ * revalidation once the freshness window expires.
+ *
+ * @see https://httpwg.org/specs/rfc7231.html#header.date
+ */
+final readonly class DateHeaderMiddleware implements HttpKernelInterface {
+
+ public function __construct(private HttpKernelInterface $httpKernel) {}
+
+ #[\Override]
+ public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = TRUE): Response {
+ $response = $this->httpKernel->handle($request, $type, $catch);
+ $response->setDate(new \DateTime());
+ return $response;
+ }
+
+}
diff --git a/assets/scaffold/local.settings.php b/assets/scaffold/local.settings.php
index 306f862c4..e8c24f225 100644
--- a/assets/scaffold/local.settings.php
+++ b/assets/scaffold/local.settings.php
@@ -54,5 +54,6 @@
$settings['telegram_token'] = NULL;
$settings['telegram_chat_id'] = NULL;
$settings['telegram_secret_token'] = NULL;
+$settings['app_foresight'] = FALSE;
$config['cache_pilot.settings']['connection_dsn'] = 'tcp://php:9000';
diff --git a/assets/vendor/foresight/build.sh b/assets/vendor/foresight/build.sh
new file mode 100755
index 000000000..6000b9e3d
--- /dev/null
+++ b/assets/vendor/foresight/build.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+# Rebuilds the ForesightJS IIFE bundle from npm.
+# Run from repo root: docker compose exec -T php bash /var/www/html/assets/vendor/foresight/build.sh
+set -euo pipefail
+
+PACKAGE="js.foresight"
+VERSION="3.5.0"
+OUT_DIR="$(cd "$(dirname "$0")" && pwd)/dist"
+TMP_DIR="$(mktemp -d)"
+
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+echo "Installing $PACKAGE@$VERSION..."
+npm install --prefix "$TMP_DIR" "$PACKAGE@$VERSION" --no-save --silent
+
+echo "Bundling with esbuild..."
+npx --prefix "$TMP_DIR" esbuild \
+ "$TMP_DIR/node_modules/$PACKAGE/dist/index.js" \
+ --bundle \
+ --format=iife \
+ --global-name=ForesightLib \
+ --minify \
+ --outfile="$OUT_DIR/foresight.umd.js"
+
+echo "Done: $OUT_DIR/foresight.umd.js ($(wc -c < "$OUT_DIR/foresight.umd.js") bytes)"
diff --git a/assets/vendor/foresight/composer.json b/assets/vendor/foresight/composer.json
new file mode 100644
index 000000000..83a2ac820
--- /dev/null
+++ b/assets/vendor/foresight/composer.json
@@ -0,0 +1,5 @@
+{
+ "name": "app-asset/foresight",
+ "type": "drupal-library",
+ "version": "3.5.0"
+}
diff --git a/assets/vendor/foresight/dist/foresight.umd.js b/assets/vendor/foresight/dist/foresight.umd.js
new file mode 100644
index 000000000..235b33042
--- /dev/null
+++ b/assets/vendor/foresight/dist/foresight.umd.js
@@ -0,0 +1,9 @@
+var ForesightLib=(()=>{var M=Object.defineProperty;var vt=Object.getOwnPropertyDescriptor;var ft=Object.getOwnPropertyNames;var mt=Object.prototype.hasOwnProperty;var g=(e,t)=>()=>(e&&(t=e(e=0)),t);var v=(e,t)=>{for(var i in t)M(e,i,{get:t[i],enumerable:!0})},pt=(e,t,i,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of ft(t))!mt.call(e,r)&&r!==i&&M(e,r,{get:()=>t[r],enumerable:!(n=vt(t,r))||n.enumerable});return e};var yt=e=>pt(M({},"__esModule",{value:!0}),e);function f(e,t,i,n){return ei&&console.warn(`ForesightJS: "${n}" value ${e} is above maximum bound ${i}, clamping to ${i}`),Math.min(Math.max(e,t),i)}function D(e){if(typeof e=="number"){let t=f(e,0,2e3,"hitslop");return{top:t,left:t,right:t,bottom:t}}return{top:f(e.top,0,2e3,"hitslop - top"),left:f(e.left,0,2e3,"hitslop - left"),right:f(e.right,0,2e3,"hitslop - right"),bottom:f(e.bottom,0,2e3,"hitslop - bottom")}}function p(e,t){return{left:e.left-t.left,right:e.right+t.right,top:e.top-t.top,bottom:e.bottom+t.bottom}}function y(e,t){return!e||!t?e===t:e.left===t.left&&e.right===t.right&&e.top===t.top&&e.bottom===t.bottom}function H(e,t){return e.x>=t.left&&e.x<=t.right&&e.y>=t.top&&e.y<=t.bottom}var O=g(()=>{});function T(e,t,i){let n=0,r=1,s=t.x-e.x,o=t.y-e.y,l=(h,c)=>{if(h===0){if(c<0)return!1}else{let a=c/h;if(h<0){if(a>r)return!1;a>n&&(n=a)}else{if(a{});var u,m=g(()=>{u=class{constructor(e){this._isConnected=!1,this._cachedLogStyle=null,this.elements=e.elements,this.callCallback=e.callCallback,this.emit=e.emit,this.hasListeners=e.hasListeners,this.settings=e.settings}get isConnected(){return this._isConnected}disconnect(){this.isConnected&&(this.devLog(`Disconnecting ${this.moduleName}...`),this.abortController?.abort(`${this.moduleName} module disconnected`),this.onDisconnect(),this._isConnected=!1)}connect(){this.devLog(`Connecting ${this.moduleName}...`),this.onConnect(),this._isConnected=!0}devLog(e){if(this.settings.enableManagerLogging){if(this._cachedLogStyle===null){let t=this.moduleName.includes("Predictor")?"#ea580c":"#2563eb";this._cachedLogStyle=`color: ${t}; font-weight: bold;`}console.log(`%c${this.moduleName}: ${e}`,this._cachedLogStyle)}}createAbortController(){this.abortController&&!this.abortController.signal.aborted||(this.abortController=new AbortController,this.devLog(`Created new AbortController for ${this.moduleName}`))}}});function Ct(e,t){let i=Math.max(e.width,F),n=Math.max(e.height,F),r=e.top-t.top-C,s=e.left-t.left-C,o=t.right-e.left-i-C,l=t.bottom-e.top-n-C;return`${-Math.round(r)}px ${-Math.round(o)}px ${-Math.round(l)}px ${-Math.round(s)}px`}function j(e){let t=e.visualViewport?.width??e.innerWidth,i=e.visualViewport?.height??e.innerHeight;return new DOMRect(0,0,t,i)}function $(e){return"nodeType"in e}function kt(e){return $(e)&&e.nodeType===Node.ELEMENT_NODE}function Et(e){return e.nodeType===Node.DOCUMENT_NODE}function Dt(e){return!e||Et(e)?e?.defaultView??window:e}function It(e,t){return t==null?!1:e.target===t.target&&e.isIntersecting===t.isIntersecting&&b.equals(e.boundingClientRect,t.boundingClientRect)&&b.equals(e.intersectionRect,t.intersectionRect)}var b,C,F,V,St,wt,Pt,Tt,U,I,_=g(()=>{b=class{static intersect(e,t){let i=Math.max(e.left,t.left),n=Math.min(e.right,t.right),r=Math.max(e.top,t.top),s=Math.min(e.bottom,t.bottom),o=Math.max(0,n-i),l=Math.max(0,s-r);return new DOMRect(i,r,o,l)}static clip(e,t){let i={...e.toJSON(),top:e.top+t.top,left:e.left+t.left,bottom:e.bottom-t.bottom,right:e.right-t.right};return i.width=i.right-i.left,i.height=i.bottom-i.top,new DOMRect(i.left,i.top,i.width,i.height)}static clipOffsets(e,t){return{top:t.top-e.top,left:t.left-e.left,bottom:e.bottom-t.bottom,right:e.right-t.right}}static equals(e,t){return e==null||t==null?e===t:e.x===t.x&&e.y===t.y&&e.width===t.width&&e.height===t.height}static sizeEqual(e,t){return Math.round(e.width)===Math.round(t.width)&&Math.round(e.height)===Math.round(t.height)}},C=-1,F=1-C*2;V=[...Array.from({length:1e3},(e,t)=>t/1e3),1],St=class{constructor(e,t,i){this.#i=t,this.#e=i,this.#n=i.clientRect,this.#r(e)}#i;#t=void 0;#e;#n;#s=void 0;get visibleRect(){let e=this.#e.clip;return e?b.clip(this.#n,e):this.#n}get isIntersecting(){let{width:e,height:t}=this.visibleRect;return e>0&&t>0}#r(e){let{root:t,rootBounds:i}=this.#e,{visibleRect:n}=this;this.#t?.disconnect(),this.#t=new IntersectionObserver(this.#o,{root:t,rootMargin:Ct(n,i),threshold:V}),this.#t.observe(e)}#o=e=>{if(!this.#t)return;let t=e[e.length-1];if(t){let{intersectionRatio:i,boundingClientRect:n}=t,r=this.#n;this.#n=n;let s=this.#s,o=!b.equals(n,r);if(i!==this.#s||o){let l=this.#e.rootBounds,h=b.intersect(n,l),c=h.width>0&&h.height>0;if(!c)return;this.#s=i,(s!=null||o)&&(this.#i(new wt(t.target,n,t.intersectionRect,c,l),this),this.#r(t.target))}}};disconnect(){this.#t?.disconnect()}},wt=class{constructor(e,t,i,n,r){this.target=e,this.boundingClientRect=t,this.intersectionRect=i,this.isIntersecting=n,this.rootBounds=r}},Pt=class{constructor(e,t){let i=Dt(e);if(kt(i)){let n=i.ownerDocument??document;this.rootBounds=i.getBoundingClientRect(),this.#i=new ResizeObserver(r=>{for(let s of r){let[{inlineSize:o,blockSize:l}]=s.borderBoxSize;if(b.sizeEqual(this.rootBounds,{width:o,height:l}))continue;let h=s.target.getBoundingClientRect();this.rootBounds=h,t(h,this)}}),this.#i.observe(i),n.addEventListener("scroll",r=>{r.target&&r.target!==i&&$(r.target)&&r.target.contains(i)&&(this.rootBounds=i.getBoundingClientRect(),t(this.rootBounds,this))},{capture:!0,passive:!0,signal:this.#t.signal})}else{let n=i.visualViewport??i;this.rootBounds=j(i);let r=()=>{let s=j(i);b.equals(this.rootBounds,s)||(this.rootBounds=s,t(s,this))};n.addEventListener("resize",r,{signal:this.#t.signal})}}#i;#t=new AbortController;rootBounds;disconnect(){this.#i?.disconnect(),this.#t.abort()}};Tt=class{constructor(e,t){this.#e=t,this.#i=i=>{let n=[];for(let r of i){let s=this.intersections.get(r.target);this.intersections.set(r.target,r),(s?.isIntersecting!==r.isIntersecting||!b.equals(s?.intersectionRect,r.intersectionRect))&&n.push(r)}n.length>0&&e(n,this)}}#i;#t=new Map;#e;intersections=new WeakMap;observe(e){let t=e.ownerDocument;if(!t)return;let i=this.#t.get(t);i||(i=new IntersectionObserver(this.#i,{...this.#e,threshold:V}),this.#t.set(t,i)),i.observe(e)}unobserve(e){let t=e.ownerDocument;if(!t)return;let i=this.#t.get(t);i&&(i.unobserve(e),this.intersections.delete(e))}disconnect(){for(let e of this.#t.values())e.disconnect();this.#t.clear()}},U=class{constructor(e,t){this.#i=e,this.#t=t,this.#r=new Pt(t?.root,this.#c),this.#o=new Tt(this.#h,t),this.#n=new ResizeObserver(this.#u)}#i;#t;#e=new Map;#n;#s=new WeakMap;#r;#o;observe(e){this.#o.observe(e)}unobserve(e){e?(this.#e.get(e)?.disconnect(),this.#o.unobserve(e),this.#s.delete(e)):this.disconnect()}disconnect(){for(let e of this.#e.values())e.disconnect();this.#e.clear(),this.#n.disconnect(),this.#r.disconnect(),this.#o.disconnect()}#a(e){let t=[];for(let i of e){let{target:n}=i,r=this.#s.get(n);It(i,r)||(this.#s.set(n,i),t.push(i))}t.length>0&&this.#i(t)}#c=e=>{let t=[];for(let[i]of this.#e){let n=i.getBoundingClientRect(),r=this.#l(i,n);t.push(new I(i,n,r.visibleRect,r.isIntersecting,e))}this.#a(t)};#l(e,t){let i=this.#o;this.#e.get(e)?.disconnect();let n=new St(e,this.#d,{clientRect:t,root:this.#t?.root,rootBounds:this.#r.rootBounds,get clip(){let r=i.intersections.get(e);if(!r)return;let{intersectionRect:s,boundingClientRect:o}=r;return b.clipOffsets(o,s)}});return this.#e.set(e,n),n}#h=e=>{let t=[];for(let i of e){let{target:n,isIntersecting:r,boundingClientRect:s}=i;r?(this.#l(n,s),this.#n.observe(n)):(this.#e.get(n)?.disconnect(),this.#e.delete(n),this.#n.unobserve(n));let o=this.#e.get(n);t.push(new I(n,s,o?.visibleRect??i.intersectionRect,r,this.#r.rootBounds))}this.#a(t)};#d=(e,t)=>{this.#a([new I(e.target,e.boundingClientRect,t.visibleRect,e.isIntersecting,this.#r.rootBounds)])};#u=e=>{let t=[];for(let i of e){let{target:n,borderBoxSize:r}=i,s=this.#s.get(n);if(s){let[{inlineSize:h,blockSize:c}]=r;if(b.sizeEqual(s.boundingClientRect,{width:h,height:c}))continue}let o=n.getBoundingClientRect(),l=this.#l(n,o);t.push(new I(n,o,l.visibleRect,this.#o.intersections.get(n)?.isIntersecting??!1,this.#r.rootBounds))}this.#a(t)}},I=class{constructor(e,t,i,n,r){this.target=e,this.boundingClientRect=t,this.intersectionRect=i,this.isIntersecting=n,this.rootBounds=r}}});var Rt,B,G,S,R,x,xt,Lt,N,K,J,Mt,Ht,Z,Ot,At,Bt,Nt,zt,Ft,jt,q,Vt,$t,Ut,W,_t,X,Y,Q=g(()=>{Rt=["input:not([inert]):not([inert] *)","select:not([inert]):not([inert] *)","textarea:not([inert]):not([inert] *)","a[href]:not([inert]):not([inert] *)","button:not([inert]):not([inert] *)","[tabindex]:not(slot):not([inert]):not([inert] *)","audio[controls]:not([inert]):not([inert] *)","video[controls]:not([inert]):not([inert] *)",'[contenteditable]:not([contenteditable="false"]):not([inert]):not([inert] *)',"details>summary:first-of-type:not([inert]):not([inert] *)","details:not([inert]):not([inert] *)"],B=Rt.join(","),G=typeof Element>"u",S=G?function(){}:Element.prototype.matches||Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector,R=!G&&Element.prototype.getRootNode?function(e){var t;return e==null||(t=e.getRootNode)===null||t===void 0?void 0:t.call(e)}:function(e){return e?.ownerDocument},x=function(t,i){var n;i===void 0&&(i=!0);var r=t==null||(n=t.getAttribute)===null||n===void 0?void 0:n.call(t,"inert"),s=r===""||r==="true",o=s||i&&t&&(typeof t.closest=="function"?t.closest("[inert]"):x(t.parentNode));return o},xt=function(t){var i,n=t==null||(i=t.getAttribute)===null||i===void 0?void 0:i.call(t,"contenteditable");return n===""||n==="true"},Lt=function(t,i,n){if(x(t))return[];var r=Array.prototype.slice.apply(t.querySelectorAll(B));return i&&S.call(t,B)&&r.unshift(t),r=r.filter(n),r},N=function(t,i,n){for(var r=[],s=Array.from(t);s.length;){var o=s.shift();if(!x(o,!1))if(o.tagName==="SLOT"){var l=o.assignedElements(),h=l.length?l:o.children,c=N(h,!0,n);n.flatten?r.push.apply(r,c):r.push({scopeParent:o,candidates:c})}else{var a=S.call(o,B);a&&n.filter(o)&&(i||!t.includes(o))&&r.push(o);var d=o.shadowRoot||typeof n.getShadowRoot=="function"&&n.getShadowRoot(o),L=!x(d,!1)&&(!n.shadowRootFilter||n.shadowRootFilter(o));if(d&&L){var E=N(d===!0?o.children:d.children,!0,n);n.flatten?r.push.apply(r,E):r.push({scopeParent:o,candidates:E})}else s.unshift.apply(s,o.children)}}return r},K=function(t){return!isNaN(parseInt(t.getAttribute("tabindex"),10))},J=function(t){if(!t)throw new Error("No node provided");return t.tabIndex<0&&(/^(AUDIO|VIDEO|DETAILS)$/.test(t.tagName)||xt(t))&&!K(t)?0:t.tabIndex},Mt=function(t,i){var n=J(t);return n<0&&i&&!K(t)?0:n},Ht=function(t,i){return t.tabIndex===i.tabIndex?t.documentOrder-i.documentOrder:t.tabIndex-i.tabIndex},Z=function(t){return t.tagName==="INPUT"},Ot=function(t){return Z(t)&&t.type==="hidden"},At=function(t){var i=t.tagName==="DETAILS"&&Array.prototype.slice.apply(t.children).some(function(n){return n.tagName==="SUMMARY"});return i},Bt=function(t,i){for(var n=0;nsummary:first-of-type"),l=o?t.parentElement:t;if(S.call(l,"details:not([open]) *"))return!0;if(!n||n==="full"||n==="full-native"||n==="legacy-full"){if(typeof r=="function"){for(var h=t;t;){var c=t.parentElement,a=R(t);if(c&&!c.shadowRoot&&r(c)===!0)return q(t);t.assignedSlot?t=t.assignedSlot:!c&&a!==t.ownerDocument?t=a.host:t=c}t=h}if(jt(t))return!t.getClientRects().length;if(n!=="legacy-full")return!0}else if(n==="non-zero-area")return q(t);return!1},$t=function(t){if(/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(t.tagName))for(var i=t.parentElement;i;){if(i.tagName==="FIELDSET"&&i.disabled){for(var n=0;n=0)},X=function(t){var i=[],n=[];return t.forEach(function(r,s){var o=!!r.scopeParent,l=o?r.scopeParent:r,h=Mt(l,o),c=o?X(r.candidates):l;h===0?o?i.push.apply(i,c):i.push(l):n.push({documentOrder:s,tabIndex:h,item:r,isScope:o,content:c})}),n.sort(Ht).reduce(function(r,s){return s.isScope?r.push.apply(r,s.content):r.push(s.content),r},[]).concat(i)},Y=function(t,i){i=i||{};var n;return i.getShadowRoot?n=N([t],i.includeContainer,{filter:W.bind(null,i),flatten:!1,getShadowRoot:i.getShadowRoot,shadowRootFilter:_t}):n=Lt(t,i.includeContainer,W.bind(null,i)),X(n)}});var tt={};v(tt,{TabPredictor:()=>Wt});function qt(e,t,i,n){if(t!==null&&t>-1){let r=e?t-1:t+1;if(r>=0&&rr===n)}var Wt,et=g(()=>{m();Q();Wt=class extends u{constructor(e){super(e),this.moduleName="TabPredictor",this.lastKeyDown=null,this.tabbableElementsCache=[],this.lastFocusedIndex=null,this.handleKeyDown=t=>{t.key==="Tab"&&(this.lastKeyDown=t)},this.handleFocusIn=t=>{if(!this.lastKeyDown)return;let i=t.target;if(!(i instanceof HTMLElement))return;(!this.tabbableElementsCache.length||this.lastFocusedIndex===-1)&&(this.devLog("Caching tabbable elements"),this.tabbableElementsCache=Y(document.documentElement));let n=this.lastKeyDown.shiftKey,r=qt(n,this.lastFocusedIndex,this.tabbableElementsCache,i);this.lastFocusedIndex=r,this.lastKeyDown=null;let s=[],o=this.settings.tabOffset,l=this.elements;for(let h=0;h<=o;h++){let c=n?r-h:r+h,a=this.tabbableElementsCache[c];a&&a instanceof Element&&l.has(a)&&s.push(a)}for(let h of s){let c=l.get(h);c&&!c.callbackInfo.isRunningCallback&&c.callbackInfo.isCallbackActive&&this.callCallback(c,{kind:"tab",subType:n?"reverse":"forwards"})}}}invalidateCache(){this.tabbableElementsCache.length&&this.devLog("Invalidating tabbable elements cache"),this.tabbableElementsCache=[],this.lastFocusedIndex=null}onConnect(){typeof document>"u"||(this.createAbortController(),document.addEventListener("keydown",this.handleKeyDown,{signal:this.abortController?.signal,passive:!0}),document.addEventListener("focusin",this.handleFocusIn,{signal:this.abortController?.signal,passive:!0}))}onDisconnect(){this.tabbableElementsCache=[],this.lastFocusedIndex=null,this.lastKeyDown=null}}});var it={};v(it,{ScrollPredictor:()=>Jt});function Gt(e,t){let i=t.top-e.top,n=t.left-e.left;return i<-1?"down":i>1?"up":n<-1?"right":n>1?"left":"none"}function Kt(e,t,i){let{x:n,y:r}=e,s={x:n,y:r};switch(t){case"down":s.y+=i;break;case"up":s.y-=i;break;case"left":s.x-=i;break;case"right":s.x+=i;break;case"none":break;default:}return s}var Jt,nt=g(()=>{A();m();Jt=class extends u{constructor(e){super(e.dependencies),this.moduleName="ScrollPredictor",this.predictedScrollPoint=null,this.scrollDirection=null,this.onDisconnect=()=>this.resetScrollProps(),this.trajectoryPositions=e.trajectoryPositions}resetScrollProps(){this.scrollDirection=null,this.predictedScrollPoint=null}handleScrollPrefetch(e,t){!e.isIntersectingWithViewport||e.callbackInfo.isRunningCallback||!e.callbackInfo.isCallbackActive||(this.scrollDirection=this.scrollDirection??Gt(e.elementBounds.originalRect,t),this.scrollDirection!=="none"&&(this.predictedScrollPoint=this.predictedScrollPoint??Kt(this.trajectoryPositions.currentPoint,this.scrollDirection,this.settings.scrollMargin),T(this.trajectoryPositions.currentPoint,this.predictedScrollPoint,e.elementBounds.expandedRect)&&this.callCallback(e,{kind:"scroll",subType:this.scrollDirection}),this.hasListeners("scrollTrajectoryUpdate")&&this.emit({type:"scrollTrajectoryUpdate",currentPoint:this.trajectoryPositions.currentPoint,predictedPoint:this.predictedScrollPoint,scrollDirection:this.scrollDirection})))}onConnect(){}}});var rt={};v(rt,{DesktopHandler:()=>Qt});function Zt(e,t,i,n){let r=performance.now();t.add({point:{x:e.x,y:e.y},time:r});let{x:s,y:o}=e;if(t.length<2){n.x=s,n.y=o;return}let[l,h]=t.getFirstLast();if(!l||!h){n.x=s,n.y=o;return}let c=(h.time-l.time)*.001;if(c===0){n.x=s,n.y=o;return}let a=h.point.x-l.point.x,d=h.point.y-l.point.y,L=a/c,E=d/c,z=i*.001;n.x=s+L*z,n.y=o+E*z}var Xt,Yt,Qt,st=g(()=>{O();A();m();_();Xt=class extends u{constructor(e){super(e.dependencies),this.moduleName="MousePredictor",this.trajectoryPositions=e.trajectoryPositions}updatePointerState(e){let t=this.trajectoryPositions.currentPoint;t.x=e.clientX,t.y=e.clientY,this.settings.enableMousePrediction?Zt(t,this.trajectoryPositions.positions,this.settings.trajectoryPredictionTime,this.trajectoryPositions.predictedPoint):(this.trajectoryPositions.predictedPoint.x=t.x,this.trajectoryPositions.predictedPoint.y=t.y)}processMouseMovement(e){this.updatePointerState(e);let t=this.settings.enableMousePrediction,i=this.trajectoryPositions.currentPoint;for(let n of this.elements.values()){if(!n.isIntersectingWithViewport||!n.callbackInfo.isCallbackActive||n.callbackInfo.isRunningCallback)continue;let r=n.elementBounds.expandedRect;if(t)T(i,this.trajectoryPositions.predictedPoint,r)&&this.callCallback(n,{kind:"mouse",subType:"trajectory"});else if(H(i,r)){this.callCallback(n,{kind:"mouse",subType:"hover"});return}}this.hasListeners("mouseTrajectoryUpdate")&&this.emit({type:"mouseTrajectoryUpdate",predictionEnabled:t,trajectoryPositions:this.trajectoryPositions})}onDisconnect(){}onConnect(){}},Yt=class{constructor(e){if(this.head=0,this.count=0,e<=0)throw new Error("CircularBuffer capacity must be greater than 0");this.capacity=e,this.buffer=new Array(e)}add(e){this.buffer[this.head]=e,this.head=(this.head+1)%this.capacity,this.counte){let i=t.slice(-e);for(let n of i)this.add(n)}else for(let i of t)this.add(i)}getAllItems(){if(this.count===0)return[];let e=new Array(this.count);if(this.count{let i=this.settings.enableScrollPrediction;for(let n of t){let r=this.elements.get(n.target);r&&(i?this.scrollPredictor?.handleScrollPrefetch(r,n.boundingClientRect):this.checkForMouseHover(r),this.handlePositionChangeDataUpdates(r,n))}i&&this.scrollPredictor?.resetScrollProps()},this.checkForMouseHover=t=>{H(this.trajectoryPositions.currentPoint,t.elementBounds.expandedRect)&&this.callCallback(t,{kind:"mouse",subType:"hover"})},this.handlePositionChangeDataUpdates=(t,i)=>{let n=[],r=i.isIntersecting;t.isIntersectingWithViewport!==r&&(n.push("visibility"),t.isIntersectingWithViewport=r),r&&!y(i.boundingClientRect,t.elementBounds.originalRect)&&(n.push("bounds"),t.elementBounds={hitSlop:t.elementBounds.hitSlop,originalRect:i.boundingClientRect,expandedRect:p(i.boundingClientRect,t.elementBounds.hitSlop)}),n.length&&this.hasListeners("elementDataUpdated")&&this.emit({type:"elementDataUpdated",elementData:t,updatedProps:n})},this.processMouseMovement=t=>this.mousePredictor.processMouseMovement(t),this.invalidateTabCache=()=>this.tabPredictor?.invalidateCache(),this.observeElement=t=>this.positionObserver?.observe(t),this.unobserveElement=t=>this.positionObserver?.unobserve(t),this.connectTabPredictor=async()=>{if(!this.tabPredictor){let{TabPredictor:t}=await Promise.resolve().then(()=>(et(),tt));this.tabPredictor=new t(this.storedDependencies),this.devLog("TabPredictor lazy loaded")}this.tabPredictor.connect()},this.connectScrollPredictor=async()=>{if(!this.scrollPredictor){let{ScrollPredictor:t}=await Promise.resolve().then(()=>(nt(),it));this.scrollPredictor=new t({dependencies:this.storedDependencies,trajectoryPositions:this.trajectoryPositions}),this.devLog("ScrollPredictor lazy loaded")}this.scrollPredictor.connect()},this.connectMousePredictor=()=>this.mousePredictor.connect(),this.disconnectTabPredictor=()=>this.tabPredictor?.disconnect(),this.disconnectScrollPredictor=()=>this.scrollPredictor?.disconnect(),this.disconnectMousePredictor=()=>this.mousePredictor.disconnect(),this.storedDependencies=e,this.mousePredictor=new Xt({dependencies:e,trajectoryPositions:this.trajectoryPositions})}onConnect(){this.settings.enableTabPrediction&&this.connectTabPredictor(),this.settings.enableScrollPrediction&&this.connectScrollPredictor(),this.connectMousePredictor(),this.positionObserver=new U(this.handlePositionChange);let e=["mouse"];this.settings.enableTabPrediction&&e.push("tab (loading...)"),this.settings.enableScrollPrediction&&e.push("scroll (loading...)"),this.devLog(`Connected predictors: [${e.join(", ")}] and PositionObserver`);for(let t of this.elements.keys())this.positionObserver.observe(t)}onDisconnect(){this.disconnectMousePredictor(),this.disconnectTabPredictor(),this.disconnectScrollPredictor(),this.positionObserver?.disconnect(),this.positionObserver=null}get loadedPredictors(){return{mouse:this.mousePredictor!==null,tab:this.tabPredictor!==null,scroll:this.scrollPredictor!==null}}}});var ot={};v(ot,{ViewportPredictor:()=>te});var te,at=g(()=>{m();te=class extends u{constructor(e){super(e),this.moduleName="ViewportPredictor",this.intersectionObserver=null,this.onConnect=()=>this.intersectionObserver=new IntersectionObserver(this.handleViewportEnter),this.observeElement=t=>this.intersectionObserver?.observe(t),this.unobserveElement=t=>this.intersectionObserver?.unobserve(t),this.handleViewportEnter=t=>{for(let i of t){if(!i.isIntersecting)continue;let n=this.elements.get(i.target);n&&(this.callCallback(n,{kind:"viewport"}),this.unobserveElement(i.target))}}}onDisconnect(){this.intersectionObserver?.disconnect(),this.intersectionObserver=null}}});var lt={};v(lt,{TouchStartPredictor:()=>ee});var ee,ct=g(()=>{m();ee=class extends u{constructor(e){super(e),this.moduleName="TouchStartPredictor",this.onConnect=()=>this.createAbortController(),this.onDisconnect=()=>{},this.handleTouchStart=t=>{let i=t.currentTarget,n=this.elements.get(i);n&&(this.callCallback(n,{kind:"touch"}),this.unobserveElement(i))}}observeElement(e){e instanceof HTMLElement&&e.addEventListener("pointerdown",this.handleTouchStart,{signal:this.abortController?.signal})}unobserveElement(e){e instanceof HTMLElement&&e.removeEventListener("pointerdown",this.handleTouchStart)}}});var ht={};v(ht,{TouchDeviceHandler:()=>ie});var ie,dt=g(()=>{m();ie=class extends u{constructor(e){super(e),this.moduleName="TouchDeviceHandler",this.viewportPredictor=null,this.touchStartPredictor=null,this.predictor=null,this.onDisconnect=()=>{this.devLog("Disconnecting touch predictor"),this.predictor?.disconnect()},this.onConnect=()=>this.setTouchPredictor(),this.observeElement=t=>this.predictor?.observeElement(t),this.unobserveElement=t=>this.predictor?.unobserveElement(t),this.storedDependencies=e}async getOrCreateViewportPredictor(){if(!this.viewportPredictor){let{ViewportPredictor:e}=await Promise.resolve().then(()=>(at(),ot));this.viewportPredictor=new e(this.storedDependencies),this.devLog("ViewportPredictor lazy loaded")}return this.viewportPredictor}async getOrCreateTouchStartPredictor(){if(!this.touchStartPredictor){let{TouchStartPredictor:e}=await Promise.resolve().then(()=>(ct(),lt));this.touchStartPredictor=new e(this.storedDependencies),this.devLog("TouchStartPredictor lazy loaded")}return this.touchStartPredictor}async setTouchPredictor(){switch(this.predictor?.disconnect(),this.settings.touchDeviceStrategy){case"viewport":this.predictor=await this.getOrCreateViewportPredictor(),this.devLog(`Connected touch strategy: ${this.settings.touchDeviceStrategy} (ViewportPredictor)`);break;case"onTouchStart":this.predictor=await this.getOrCreateTouchStartPredictor(),this.devLog(`Connected touch strategy: ${this.settings.touchDeviceStrategy} (TouchStartPredictor)`);break;case"none":this.predictor=null,this.devLog('Touch strategy set to "none" - no predictor connected');return;default:this.settings.touchDeviceStrategy}this.predictor?.connect();for(let e of this.elements.keys())this.predictor?.observeElement(e)}get loadedPredictors(){return{viewport:this.viewportPredictor!==null,touchStart:this.touchStartPredictor!==null}}}});var ge={};v(ge,{ForesightManager:()=>bt});O();function ne(){let e=ut(),t=re();return{isTouchDevice:e,isLimitedConnection:t,shouldRegister:!t}}function ut(){return typeof window>"u"||typeof navigator>"u"?!1:window.matchMedia("(pointer: coarse)").matches&&navigator.maxTouchPoints>0}function re(){let e=navigator.connection;if(!e)return!1;let t=bt.instance.getManagerData.globalSettings.minimumConnectionType,i=["slow-2g","2g","3g","4g"],n=i.indexOf(e.effectiveType),r=i.indexOf(t);return n"u"||typeof document>"u")return!1;let t=window.innerWidth||document.documentElement.clientWidth,i=window.innerHeight||document.documentElement.clientHeight;return e.top0&&e.left0}function oe(){return{mouse:{hover:0,trajectory:0},tab:{forwards:0,reverse:0},scroll:{down:0,left:0,right:0,up:0},touch:0,viewport:0,total:0}}function ae(){return{debug:!1,enableManagerLogging:!1,enableMousePrediction:!0,enableScrollPrediction:!0,positionHistorySize:8,trajectoryPredictionTime:120,scrollMargin:150,defaultHitSlop:{top:0,left:0,right:0,bottom:0},enableTabPrediction:!0,tabOffset:2,touchDeviceStrategy:"onTouchStart",minimumConnectionType:"3g"}}function le(e,t,i){let{element:n,callback:r,hitSlop:s,name:o,meta:l,reactivateAfter:h}=e,c=n.getBoundingClientRect(),a=s?D(s):i;return{id:t,element:n,callback:r,elementBounds:{originalRect:c,expandedRect:p(c,a),hitSlop:a},name:o||n.id||"unnamed",isIntersectingWithViewport:se(c),registerCount:1,meta:l??{},callbackInfo:{callbackFiredCount:0,lastCallbackInvokedAt:void 0,lastCallbackCompletedAt:void 0,lastCallbackRuntime:void 0,lastCallbackStatus:void 0,lastCallbackErrorMessage:void 0,reactivateAfter:h??1/0,isCallbackActive:!0,isRunningCallback:!1,reactivateTimeoutId:void 0}}}var ce=class{constructor(){this.eventListeners=new Map}addEventListener(e,t,i){if(i?.signal?.aborted)return;let n=this.eventListeners.get(e)??[];n.push(t),this.eventListeners.set(e,n),i?.signal?.addEventListener("abort",()=>this.removeEventListener(e,t))}removeEventListener(e,t){let i=this.eventListeners.get(e);if(!i)return;let n=i.indexOf(t);n>-1&&i.splice(n,1)}emit(e){let t=this.eventListeners.get(e.type);if(!(!t||t.length===0))for(let i=0;i0}getEventListeners(){return this.eventListeners}};function gt(e,t){return e!==void 0&&t!==e}var he={trajectoryPredictionTime:{min:10,max:200},positionHistorySize:{min:2,max:30},scrollMargin:{min:30,max:300},tabOffset:{min:0,max:20}};function w(e,t,i){if(!gt(i,e[t]))return!1;let{min:n,max:r}=he[t];return e[t]=f(i,n,r,t),!0}function P(e,t,i){return gt(i,e[t])?(e[t]=i,!0):!1}function de(e,t){w(e,"trajectoryPredictionTime",t.trajectoryPredictionTime),w(e,"positionHistorySize",t.positionHistorySize),w(e,"scrollMargin",t.scrollMargin),w(e,"tabOffset",t.tabOffset),P(e,"enableMousePrediction",t.enableMousePrediction),P(e,"enableScrollPrediction",t.enableScrollPrediction),P(e,"enableTabPrediction",t.enableTabPrediction),P(e,"enableManagerLogging",t.enableManagerLogging),t.defaultHitSlop!==void 0&&(e.defaultHitSlop=D(t.defaultHitSlop)),t.touchDeviceStrategy!==void 0&&(e.touchDeviceStrategy=t.touchDeviceStrategy),t.minimumConnectionType!==void 0&&(e.minimumConnectionType=t.minimumConnectionType),t.debug!==void 0&&(e.debug=t.debug)}function ue(e,t){let i=[],n=!1,r=!1,s=!1,o=!1,l=!1;if(!t)return{changedSettings:i,positionHistorySizeChanged:n,scrollPredictionChanged:r,tabPredictionChanged:s,hitSlopChanged:o,touchStrategyChanged:l};let h=["trajectoryPredictionTime","positionHistorySize","scrollMargin","tabOffset"];for(let a of h){let d=e[a];w(e,a,t[a])&&(i.push({setting:a,oldValue:d,newValue:e[a]}),a==="positionHistorySize"&&(n=!0))}let c=["enableMousePrediction","enableScrollPrediction","enableTabPrediction"];for(let a of c){let d=e[a];P(e,a,t[a])&&(i.push({setting:a,oldValue:d,newValue:e[a]}),a==="enableScrollPrediction"&&(r=!0),a==="enableTabPrediction"&&(s=!0))}if(t.defaultHitSlop!==void 0){let a=e.defaultHitSlop,d=D(t.defaultHitSlop);y(a,d)||(e.defaultHitSlop=d,i.push({setting:"defaultHitSlop",oldValue:a,newValue:d}),o=!0)}if(t.touchDeviceStrategy!==void 0){let a=e.touchDeviceStrategy;e.touchDeviceStrategy=t.touchDeviceStrategy,i.push({setting:"touchDeviceStrategy",oldValue:a,newValue:t.touchDeviceStrategy}),l=!0}if(t.minimumConnectionType!==void 0){let a=e.minimumConnectionType;e.minimumConnectionType=t.minimumConnectionType,i.push({setting:"minimumConnectionType",oldValue:a,newValue:t.minimumConnectionType})}return{changedSettings:i,positionHistorySizeChanged:n,scrollPredictionChanged:r,tabPredictionChanged:s,hitSlopChanged:o,touchStrategyChanged:l}}var bt=class k{constructor(t){this.elements=new Map,this.checkableElements=new Set,this.idCounter=0,this.activeElementCount=0,this.desktopHandler=null,this.touchDeviceHandler=null,this.currentlyActiveHandler=null,this.isSetup=!1,this.pendingPointerEvent=null,this.rafId=null,this.domObserver=null,this.currentDeviceStrategy=ut()?"touch":"mouse",this.eventEmitter=new ce,this._globalCallbackHits=oe(),this._globalSettings=ae(),this.handlePointerMove=i=>{this.pendingPointerEvent=i,i.pointerType!==this.currentDeviceStrategy&&(this.eventEmitter.emit({type:"deviceStrategyChanged",timestamp:Date.now(),newStrategy:i.pointerType,oldStrategy:this.currentDeviceStrategy}),this.currentDeviceStrategy=i.pointerType,this.setDeviceStrategy(this.currentDeviceStrategy)),!this.rafId&&(this.rafId=requestAnimationFrame(()=>{if(!this.isUsingDesktopHandler){this.rafId=null;return}this.pendingPointerEvent&&this.desktopHandler?.processMouseMovement(this.pendingPointerEvent),this.rafId=null}))},this.handleDomMutations=i=>{if(!i.length)return;this.desktopHandler?.invalidateTabCache();let n=!1;for(let r=0;r0){n=!0;break}}if(n)for(let r of this.elements.keys())r.isConnected||this.unregister(r,"disconnected")},t!==void 0&&de(this._globalSettings,t),this.handlerDependencies={elements:this.elements,callCallback:this.callCallback.bind(this),emit:this.eventEmitter.emit.bind(this.eventEmitter),hasListeners:this.eventEmitter.hasListeners.bind(this.eventEmitter),settings:this._globalSettings},this.devLog(`ForesightManager initialized with device strategy: ${this.currentDeviceStrategy}`),this.initializeGlobalListeners()}async getOrCreateDesktopHandler(){if(!this.desktopHandler){let{DesktopHandler:t}=await Promise.resolve().then(()=>(st(),rt));this.desktopHandler=new t(this.handlerDependencies),this.devLog("DesktopHandler lazy loaded")}return this.desktopHandler}async getOrCreateTouchHandler(){if(!this.touchDeviceHandler){let{TouchDeviceHandler:t}=await Promise.resolve().then(()=>(dt(),ht));this.touchDeviceHandler=new t(this.handlerDependencies),this.devLog("TouchDeviceHandler lazy loaded")}return this.touchDeviceHandler}static initialize(t){return this.isInitiated||(k.manager=new k(t)),k.manager}static get isInitiated(){return!!k.manager}static get instance(){return this.initialize()}generateId(){return`foresight-${++this.idCounter}`}get isUsingDesktopHandler(){return this.currentDeviceStrategy==="mouse"||this.currentDeviceStrategy==="pen"}addEventListener(t,i,n){this.eventEmitter.addEventListener(t,i,n)}removeEventListener(t,i){this.eventEmitter.removeEventListener(t,i)}hasListeners(t){return this.eventEmitter.hasListeners(t)}get getManagerData(){let t=this.desktopHandler?.loadedPredictors,i=this.touchDeviceHandler?.loadedPredictors;return{registeredElements:this.elements,globalSettings:this._globalSettings,globalCallbackHits:this._globalCallbackHits,eventListeners:this.eventEmitter.getEventListeners(),currentDeviceStrategy:this.currentDeviceStrategy,activeElementCount:this.activeElementCount,loadedModules:{desktopHandler:this.desktopHandler!==null,touchHandler:this.touchDeviceHandler!==null,predictors:{mouse:t?.mouse??!1,tab:t?.tab??!1,scroll:t?.scroll??!1,viewport:i?.viewport??!1,touchStart:i?.touchStart??!1}}}}get registeredElements(){return this.elements}register(t){let{element:i,...n}=t;return i instanceof NodeList?Array.from(i,r=>this.registerElement({...n,element:r})):this.registerElement({...n,element:i})}registerElement(t){let{isTouchDevice:i,isLimitedConnection:n,shouldRegister:r}=ne();if(!r)return{isLimitedConnection:n,isTouchDevice:i,isRegistered:!1,unregister:()=>{},elementData:null};let s=this.elements.get(t.element);if(s)return s.registerCount++,{isLimitedConnection:n,isTouchDevice:i,isRegistered:!1,unregister:()=>{},elementData:null};this.isSetup||this.initializeGlobalListeners();let o=le(t,this.generateId(),this._globalSettings.defaultHitSlop);return this.elements.set(t.element,o),this.activeElementCount++,this.updateCheckableStatus(o),this.currentlyActiveHandler?.observeElement(t.element),this.eventEmitter.emit({type:"elementRegistered",timestamp:Date.now(),elementData:o}),{isTouchDevice:i,isLimitedConnection:n,isRegistered:!0,unregister:()=>{this.unregister(t.element)},elementData:o}}unregister(t,i){t instanceof NodeList?t.forEach(n=>this.unregisterElement(n,i)):this.unregisterElement(t,i)}unregisterElement(t,i){let n=this.elements.get(t);if(!n)return;this.clearReactivateTimeout(n),this.currentlyActiveHandler?.unobserveElement(t),this.elements.delete(t),this.checkableElements.delete(n),n.callbackInfo.isCallbackActive&&this.activeElementCount--;let r=this.elements.size===0&&this.isSetup;r&&(this.devLog("All elements unregistered, removing global listeners"),this.removeGlobalListeners()),this.eventEmitter.emit({type:"elementUnregistered",elementData:n,timestamp:Date.now(),unregisterReason:i??"by user",wasLastRegisteredElement:r})}reactivate(t){t instanceof NodeList?t.forEach(i=>this.reactivateElement(i)):this.reactivateElement(t)}reactivateElement(t){let i=this.elements.get(t);i&&(this.isSetup||this.initializeGlobalListeners(),this.clearReactivateTimeout(i),i.callbackInfo.isRunningCallback||(i.callbackInfo.isCallbackActive=!0,this.activeElementCount++,this.updateCheckableStatus(i),this.currentlyActiveHandler?.observeElement(t),this.eventEmitter.emit({type:"elementReactivated",elementData:i,timestamp:Date.now()})))}clearReactivateTimeout(t){clearTimeout(t.callbackInfo.reactivateTimeoutId),t.callbackInfo.reactivateTimeoutId=void 0}updateCheckableStatus(t){t.isIntersectingWithViewport&&t.callbackInfo.isCallbackActive&&!t.callbackInfo.isRunningCallback?this.checkableElements.add(t):this.checkableElements.delete(t)}callCallback(t,i){t.callbackInfo.isRunningCallback||!t.callbackInfo.isCallbackActive||(this.markElementAsRunning(t),this.executeCallbackAsync(t,i))}markElementAsRunning(t){t.callbackInfo.callbackFiredCount++,t.callbackInfo.lastCallbackInvokedAt=Date.now(),t.callbackInfo.isRunningCallback=!0,this.clearReactivateTimeout(t),this.checkableElements.delete(t)}async executeCallbackAsync(t,i){this.updateHitCounters(i),this.eventEmitter.emit({type:"callbackInvoked",timestamp:Date.now(),elementData:t,hitType:i});let n=performance.now(),r,s=null;try{await t.callback(t),r="success"}catch(o){s=o instanceof Error?o.message:String(o),r="error",console.error(`Error in callback for element ${t.name}:`,o)}this.finalizeCallback(t,i,n,r,s)}finalizeCallback(t,i,n,r,s){t.callbackInfo.lastCallbackCompletedAt=Date.now(),t.callbackInfo.isRunningCallback=!1,t.callbackInfo.isCallbackActive=!1,this.activeElementCount--,this.currentlyActiveHandler?.unobserveElement(t.element),t.callbackInfo.reactivateAfter!==1/0&&(t.callbackInfo.reactivateTimeoutId=setTimeout(()=>{this.reactivate(t.element)},t.callbackInfo.reactivateAfter));let o=this.activeElementCount===0;o&&(this.devLog("All elements unactivated, removing global listeners"),this.removeGlobalListeners()),this.eventEmitter.emit({type:"callbackCompleted",timestamp:Date.now(),elementData:t,hitType:i,elapsed:t.callbackInfo.lastCallbackRuntime=performance.now()-n,status:t.callbackInfo.lastCallbackStatus=r,errorMessage:t.callbackInfo.lastCallbackErrorMessage=s,wasLastActiveElement:o})}updateHitCounters(t){switch(t.kind){case"mouse":this._globalCallbackHits.mouse[t.subType]++;break;case"tab":this._globalCallbackHits.tab[t.subType]++;break;case"scroll":this._globalCallbackHits.scroll[t.subType]++;break;case"touch":this._globalCallbackHits.touch++;break;case"viewport":this._globalCallbackHits.viewport++;break;default:}this._globalCallbackHits.total++}async setDeviceStrategy(t){let i=this.currentDeviceStrategy;i!==t&&this.devLog(`Switching device strategy from ${i} to ${t}`),this.currentlyActiveHandler?.disconnect(),this.currentlyActiveHandler=t==="mouse"||t==="pen"?await this.getOrCreateDesktopHandler():await this.getOrCreateTouchHandler(),this.currentlyActiveHandler.connect()}initializeGlobalListeners(){this.isSetup||typeof document>"u"||(this.devLog("Initializing global listeners (pointermove, MutationObserver)"),this.setDeviceStrategy(this.currentDeviceStrategy),document.addEventListener("pointermove",this.handlePointerMove),this.domObserver=new MutationObserver(this.handleDomMutations),this.domObserver.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!1}),this.isSetup=!0)}removeGlobalListeners(){typeof document>"u"||(this.isSetup=!1,this.domObserver?.disconnect(),this.domObserver=null,document.removeEventListener("pointermove",this.handlePointerMove),this.currentlyActiveHandler?.disconnect(),this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.pendingPointerEvent=null)}alterGlobalSettings(t){let i=ue(this._globalSettings,t);i.positionHistorySizeChanged&&this.desktopHandler&&this.desktopHandler.trajectoryPositions.positions.resize(this._globalSettings.positionHistorySize),i.scrollPredictionChanged&&this.isUsingDesktopHandler&&this.desktopHandler&&(this._globalSettings.enableScrollPrediction?this.desktopHandler.connectScrollPredictor():this.desktopHandler.disconnectScrollPredictor()),i.tabPredictionChanged&&this.isUsingDesktopHandler&&this.desktopHandler&&(this._globalSettings.enableTabPrediction?this.desktopHandler.connectTabPredictor():this.desktopHandler.disconnectTabPredictor()),i.hitSlopChanged&&this.forceUpdateAllElementBounds(),i.touchStrategyChanged&&!this.isUsingDesktopHandler&&this.touchDeviceHandler&&this.touchDeviceHandler.setTouchPredictor(),i.changedSettings.length>0&&this.eventEmitter.emit({type:"managerSettingsChanged",timestamp:Date.now(),managerData:this.getManagerData,updatedSettings:i.changedSettings})}forceUpdateAllElementBounds(){for(let t of this.elements.values())t.isIntersectingWithViewport&&this.forceUpdateElementBounds(t)}forceUpdateElementBounds(t){let i=t.element.getBoundingClientRect(),n=p(i,t.elementBounds.hitSlop);if(!y(n,t.elementBounds.expandedRect)){let r={...t,elementBounds:{...t.elementBounds,originalRect:i,expandedRect:n}};this.elements.set(t.element,r),this.eventEmitter.emit({type:"elementDataUpdated",elementData:r,updatedProps:["bounds"]})}}devLog(t){this._globalSettings.enableManagerLogging&&console.log(`%c\u{1F6E0}\uFE0F ForesightManager: ${t}`,"color: #16a34a; font-weight: bold;")}};return yt(ge);})();
+/*! Bundled license information:
+
+tabbable/dist/index.esm.js:
+ (*!
+ * tabbable 6.4.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ *)
+*/
diff --git a/composer.lock b/composer.lock
index 79b350320..14bc1493e 100644
--- a/composer.lock
+++ b/composer.lock
@@ -19,6 +19,19 @@
"relative": true
}
},
+ {
+ "name": "app-asset/foresight",
+ "version": "3.5.0",
+ "dist": {
+ "type": "path",
+ "url": "assets/vendor/foresight",
+ "reference": "18273468d4fae88e3063080fcc13a2f48dcf74f5"
+ },
+ "type": "drupal-library",
+ "transport-options": {
+ "relative": true
+ }
+ },
{
"name": "app-asset/hljs",
"version": "11.11.1",
@@ -272,9 +285,10 @@
"dist": {
"type": "path",
"url": "app/modules/platform",
- "reference": "2dcfe97539bbc54cb1b3b26f099ac0d5229f301a"
+ "reference": "4702b12f3f3a2c8774d30b0a32f135a68f6b8a0c"
},
"require": {
+ "app-asset/foresight": "^3.5",
"app-asset/photoswipe": "^5.4",
"app/contract": "^1.0",
"league/html-to-markdown": "^5.1"
diff --git a/config/.eslintrc.json b/config/.eslintrc.json
index 92803dd17..73b811839 100644
--- a/config/.eslintrc.json
+++ b/config/.eslintrc.json
@@ -10,7 +10,8 @@
],
"globals": {
"ColorScheme": true,
- "AlpineOnce": true
+ "AlpineOnce": true,
+ "ForesightLib": true
},
"rules": {
"prettier/prettier": "off",
diff --git a/config/cspell/dictionary.txt b/config/cspell/dictionary.txt
index 7b466c1e2..a9f021dfe 100644
--- a/config/cspell/dictionary.txt
+++ b/config/cspell/dictionary.txt
@@ -16,9 +16,11 @@ lightbox
LLMS
Llms
llms
+foresightjs
Malyshev
Niklan
niklan
+noprefetch
Nutgram
opsz
Photoswipe
diff --git a/config/sync/core.extension.yml b/config/sync/core.extension.yml
index 350edb603..ce4f7ac78 100644
--- a/config/sync/core.extension.yml
+++ b/config/sync/core.extension.yml
@@ -58,7 +58,6 @@ module:
path_alias: 0
photoswipe: 0
phpass: 0
- quicklink: 0
rabbit_hole: 0
redirect: 0
redirect_404: 0
diff --git a/config/sync/quicklink.settings.yml b/config/sync/quicklink.settings.yml
deleted file mode 100644
index a95b4c4d0..000000000
--- a/config/sync/quicklink.settings.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-_core:
- default_config_hash: oLgmGR2Jq98YjM80b7XIU7o_u9PTxwj0P23G60FQiaE
-no_load_when_authenticated: true
-no_load_when_session: true
-no_load_content_types: { }
-ignore_admin_paths: true
-ignore_hashes: true
-ignore_ajax_links: true
-ignore_file_ext: true
-ignore_selectors: ''
-load_polyfill: false
-selector: ''
-url_patterns_to_ignore: ''
-prefetch_only_paths: ''
-allowed_domains: ''
-enable_debug_mode: false
-total_request_limit: 0
-concurrency_throttle_limit: 0
-idle_wait_timeout: 2000
-viewport_delay: 0