@prefetchru/prefetch 1.0.6 → 1.0.8

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # prefetch.ru
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@prefetchru/prefetch?color=0969da)](https://www.npmjs.com/package/@prefetchru/prefetch)
4
+ [![downloads](https://img.shields.io/npm/dm/@prefetchru/prefetch?color=0969da)](https://www.npmjs.com/package/@prefetchru/prefetch)
5
+ [![size](https://img.shields.io/bundlephobia/minzip/@prefetchru/prefetch?color=0969da&label=gzip)](https://bundlephobia.com/package/@prefetchru/prefetch)
6
+ [![license](https://img.shields.io/npm/l/@prefetchru/prefetch?color=0969da)](LICENSE)
7
+ [![release](https://img.shields.io/github/v/release/prefetch-ru/prefetch?color=0969da)](https://github.com/prefetch-ru/prefetch/releases)
8
+ [![updated](https://img.shields.io/github/last-commit/prefetch-ru/prefetch?color=0969da&label=updated)](https://github.com/prefetch-ru/prefetch/commits)
9
+ [![lang](https://img.shields.io/badge/язык-русский-0969da)](https://prefetch.ru)
10
+
3
11
  **Instant page loading for Russian web**
4
12
 
5
13
  Prefetch pages 65ms before user clicks. Pre-loads links on hover with smart fallbacks for iOS/Safari, optimized for Russian browsers (Yandex.Browser, Atom), CMS platforms (1C-Bitrix, Tilda), and e-commerce. Works everywhere with zero configuration — just add one script tag.
@@ -32,7 +40,38 @@ npm install @prefetchru/prefetch
32
40
  ```
33
41
 
34
42
  ```javascript
43
+ // ESM (рекомендуется)
35
44
  import '@prefetchru/prefetch'
45
+
46
+ // CommonJS
47
+ require('@prefetchru/prefetch')
48
+ ```
49
+
50
+ ### ESM версия
51
+
52
+ npm пакет включает ESM версию с поддержкой SSR и автоматическим определением CSP nonce:
53
+
54
+ | Файл | Описание |
55
+ |------|----------|
56
+ | `prefetch.js` | IIFE версия (классический `<script>`) |
57
+ | `prefetch.esm.js` | ESM версия (для бандлеров и `<script type="module">`) |
58
+ | `dist/prefetch.min.js` | Минифицированная IIFE |
59
+ | `dist/prefetch.esm.min.js` | Минифицированная ESM |
60
+
61
+ **Важно о CSP nonce:**
62
+
63
+ ESM версия автоматически определяет `nonce` через `import.meta.url`, но это работает только при прямом подключении:
64
+
65
+ ```html
66
+ <!-- ✅ nonce определится автоматически -->
67
+ <script type="module" src="prefetch.esm.js" nonce="abc123"></script>
68
+ ```
69
+
70
+ При импорте из бандлера (Vite, Webpack, Rollup) `import.meta.url` указывает на бандл, а не на исходный скрипт. В этом случае передавайте nonce через `data-prefetch-nonce`:
71
+
72
+ ```html
73
+ <!-- При использовании через бандлер -->
74
+ <body data-prefetch-nonce="{{RANDOM_NONCE}}">
36
75
  ```
37
76
 
38
77
  ### CDN
@@ -98,7 +137,7 @@ import '@prefetchru/prefetch'
98
137
  // Доступен объект window.Prefetch после загрузки скрипта
99
138
 
100
139
  // Версия библиотеки
101
- console.log(Prefetch.version) // "1.0.5"
140
+ console.log(Prefetch.version) // "1.0.8"
102
141
 
103
142
  // Программная предзагрузка URL
104
143
  Prefetch.preload('/catalog/product-123')
@@ -0,0 +1,6 @@
1
+ /*!
2
+ * prefetch.ru v1.0.8 (ESM) - Мгновенная загрузка страниц
3
+ * © 2026 Сергей Макаров | MIT License
4
+ * https://prefetch.ru | https://github.com/prefetch-ru
5
+ */
6
+ var e="undefined"!=typeof window&&window.Prefetch?window.Prefetch:function(e){if("undefined"==typeof window||"undefined"==typeof document)return{version:"1.0.8",preload:function(){}};var t=new Set,n=new WeakMap,r=0,i=0,o=null,a=!1,c=!1,u=null,l=null,s=!1,f=null,d=null,h=!1,m=65,p=80,v=50,g=!1,w=!1,y=!1,x=!1,b="none",L=!1,E=!1,T=!1,S=!1;function O(){var e=document.body;if(e){l=void 0!==window.BX?"bitrix":void 0!==window.B24||void 0!==window.BX24?"bitrix24":document.querySelector(".t-records")||void 0!==window.Tilda?"tilda":null;var t=e.dataset;if(g="prefetchAllowQueryString"in t||"instantAllowQueryString"in t,w="prefetchAllowExternalLinks"in t||"instantAllowExternalLinks"in t,y="prefetchWhitelist"in t||"instantWhitelist"in t,t.prefetchNonce&&(d=t.prefetchNonce),!d&&t.instantNonce&&(d=t.instantNonce),!c&&("prefetchSpecrules"in t||"instantSpecrules"in t)&&HTMLScriptElement.supports&&HTMLScriptElement.supports("speculationrules")){var n=t.prefetchSpecrules||t.instantSpecrules;"prerender"===n?(b="prerender",x=!0):"no"!==n&&(b="prefetch",x=!0)}L="prefetchPrerenderAll"in t||"instantPrerenderAll"in t;var r=t.prefetchIntensity||t.instantIntensity;if("mousedown"===r)E=!0;else if("viewport"===r||"viewport-all"===r)("viewport-all"===r||a&&A())&&(T=!0);else if(r){var i=parseInt(r,10);!isNaN(i)&&i>=0&&(m=i)}a&&(p=Math.max(60,Math.min(m||0,150))),(S="prefetchObserveDom"in t)||"bitrix"!==l&&"tilda"!==l||(S=!0),"tilda"===l&&m<100&&(m=100);var o={capture:!0,passive:!0};if(document.addEventListener("touchstart",P,o),E?document.addEventListener("mousedown",C,o):document.addEventListener("mouseover",N,o),T)(window.requestIdleCallback||function(e){setTimeout(e,1)})(U,{timeout:1500});S&&function(){if(H)return;(H=new MutationObserver(function(e){e.some(function(e){return Array.from(e.addedNodes).some(function(e){return 1===e.nodeType&&("A"===e.tagName||e.querySelectorAll&&e.querySelectorAll("a").length)})})&&R&&(clearTimeout(Q),Q=setTimeout(z,100))})).observe(document.body,{childList:!0,subtree:!0})}()}}function A(){return!s&&("slow-2g"!==f&&"2g"!==f)}function k(e){return e?(e.nodeType&&1!==e.nodeType&&(e=e.parentElement),e&&"function"==typeof e.closest?e.closest("a"):null):null}function P(e){r=e.timeStamp||Date.now();var t=k(e.target);if(I(t)){i&&(clearTimeout(i),i=0),o&&(document.removeEventListener("touchmove",o,!0),document.removeEventListener("scroll",o,!0),o=null);var n=!1;o=function(){n=!0,i&&(clearTimeout(i),i=0),document.removeEventListener("touchmove",o,!0),document.removeEventListener("scroll",o,!0),o=null},document.addEventListener("touchmove",o,{capture:!0,passive:!0,once:!0}),document.addEventListener("scroll",o,{capture:!0,passive:!0,once:!0}),i=setTimeout(function(){o&&(document.removeEventListener("touchmove",o,!0),document.removeEventListener("scroll",o,!0),o=null),i=0,n||q(t.href,t)},p)}}function N(e){if(!(r&&e.timeStamp&&e.timeStamp-r<2500)){var t=k(e.target);if(I(t)&&!n.has(t)){t.addEventListener("mouseleave",M,{passive:!0,once:!0});var i=setTimeout(function(){q(t.href,t),n.delete(t)},m);n.set(t,i)}}}function M(e){var t=e.currentTarget;if(t){var r=n.get(t);r&&(clearTimeout(r),n.delete(t))}}function C(e){if(!("number"==typeof e.button&&2===e.button||r&&e.timeStamp&&e.timeStamp-r<2500)){var t=k(e.target);I(t)&&q(t.href,t)}}function I(e){if(!e)return!1;var n=e.getAttribute("href");if(null===n||""===n.trim())return!1;if(!e.href)return!1;if(e.target&&"_self"!==e.target)return!1;if(e.hasAttribute("download"))return!1;if("noPrefetch"in e.dataset||"prefetchNo"in e.dataset)return!1;if(y&&!("prefetch"in e.dataset)&&!("instant"in e.dataset))return!1;if("http:"!==e.protocol&&"https:"!==e.protocol)return!1;if("http:"===e.protocol&&"https:"===location.protocol)return!1;if(e.origin!==location.origin){if(!w&&!("prefetch"in e.dataset)&&!("instant"in e.dataset))return!1;if(!u)return!1}if(e.search&&!g&&!("prefetch"in e.dataset)&&!("instant"in e.dataset))return!1;if(e.hash&&e.pathname+e.search===location.pathname+location.search)return!1;if(W(e.href)===W(location.href))return!1;var r=W(e.href);return!t.has(r)&&(!!function(e){var t=e.href,n="",r="";try{var i=new URL(t,location.href);n=i.pathname||"",r=i.hash||""}catch(e){n="",r=""}if("bitrix"===l||"bitrix24"===l){if(-1!==t.indexOf("/bitrix/")||-1!==t.indexOf("sessid="))return!1;if(e.classList.contains("bx-ajax"))return!1}if("tilda"===l&&(-1!==r.indexOf("#popup:")||-1!==r.indexOf("#rec")))return!1;var o=/(^|\/)(add|delete|remove)(\/|$|\.)/i.test(n);return-1===t.indexOf("/login")&&-1===t.indexOf("/logout")&&-1===t.indexOf("/auth")&&-1===t.indexOf("/register")&&-1===t.indexOf("/cart")&&-1===t.indexOf("/basket")&&!o&&!/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)/i.test(t)}(e)&&!!function(e){var t=e.href,n=e.className||"",r="";try{r=new URL(t,location.href).hostname}catch(e){r=""}return-1===n.indexOf("ym-")&&("mc.yandex.ru"!==r&&"metrika.yandex.ru"!==r&&(-1===n.indexOf("ga-")&&-1===n.indexOf("gtm-")&&("google-analytics.com"!==r&&!r.endsWith(".google-analytics.com")&&("googletagmanager.com"!==r&&!r.endsWith(".googletagmanager.com")&&(-1===n.indexOf("piwik")&&-1===n.indexOf("matomo")&&("matomo.org"!==r&&!r.endsWith(".matomo.org")&&"piwik.org"!==r&&!r.endsWith(".piwik.org")))))))}(e))}function W(e){try{var t=new URL(e,location.href);return t.origin+t.pathname+t.search}catch(t){return e}}function q(e,n){if(A()){var r=function(e){try{var t=new URL(e,location.href);return t.hash="",t.href}catch(t){return e}}(e),i=W(r);if(!t.has(i)){t.size>=v&&t.delete(t.values().next().value),t.add(i);var o=function(e){return x&&"none"!==b?"prerender"!==b?b:L||y||e&&e.dataset&&("prefetchPrerender"in e.dataset||"instantPrerender"in e.dataset)?"prerender":"prefetch":"none"}(n);if("none"!==o)return function(e,t){var n=document.head;if(!n)return;var r=document.createElement("script");r.type="speculationrules",d&&(r.nonce=d);var i={};i[t]=[{source:"list",urls:[e]}],r.textContent=JSON.stringify(i),n.appendChild(r)}(r,o),void(c||!h?D(r,i):B(r,i));c||!h?D(r,i):B(r,i)}}}function B(e,n){var r=document.head;if(r){var i=document.createElement("link");i.rel="prefetch",i.href=e,i.as="document";try{i.fetchPriority="low"}catch(e){}i.onerror=function(){t.delete(n)},r.appendChild(i)}}function D(e,n){if("function"==typeof fetch){var r=null,i=0;"undefined"!=typeof AbortController&&(r=new AbortController,i=setTimeout(function(){try{r.abort()}catch(e){}},5e3));var o={method:"GET",credentials:"same-origin",cache:"force-cache",headers:{Purpose:"prefetch"}};r&&(o.signal=r.signal);try{fetch(e,o).then(function(e){i&&clearTimeout(i),e&&e.ok||t.delete(n)}).catch(function(){i&&clearTimeout(i),t.delete(n)})}catch(e){i&&clearTimeout(i),t.delete(n)}}}!function(){if(!(d=function(e){try{if(!e)return null;for(var t=document.getElementsByTagName("script"),n=0;n<t.length;n++){var r=t[n];if(r&&r.src&&r.src===e){var i=r.nonce||r.getAttribute("nonce")||null;if(i)return i;break}}}catch(e){}return null}(e)))try{var t=document.currentScript;t&&t.nonce&&(d=t.nonce)}catch(e){}try{var n=document.createElement("link");n.relList&&"function"==typeof n.relList.supports&&(h=n.relList.supports("prefetch"))}catch(e){}var r=navigator.userAgent;c=/iPad|iPhone/.test(r)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1;var i=/Android/.test(r);(a=(c||i)&&Math.min(screen.width,screen.height)<768)&&(v=20);var o=r.match(/Chrome\/(\d+)/);o&&(u=parseInt(o[1],10));var l=navigator.connection;l&&(f=l.effectiveType,s=l.saveData||!1),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",O):O()}();var R=null;function U(){R||(R=new IntersectionObserver(function(e){e.forEach(function(e){e.isIntersecting&&(R.unobserve(e.target),I(e.target)&&q(e.target.href,e.target))})},{rootMargin:a?"100px":"200px"}),z())}function z(){R&&document.querySelectorAll("a").forEach(function(e){I(e)&&R.observe(e)})}var H=null,Q=null,X={version:"1.0.8",preload:function(e){q(e)}};return window.Prefetch=X,X}(import.meta.url);export{e as Prefetch};export default e;
@@ -1,6 +1,6 @@
1
1
  /*!
2
- * prefetch.ru v1.0.6 - Мгновенная загрузка страниц
2
+ * prefetch.ru v1.0.8 - Мгновенная загрузка страниц
3
3
  * © 2026 Сергей Макаров | MIT License
4
4
  * https://prefetch.ru | https://github.com/prefetch-ru
5
5
  */
6
- !function(){"use strict";var e=new Set,t=new WeakMap,n=0,r=0,i=null,o=!1,a=!1,c=null,u=null,s=!1,l=null,d=null,f=!1,m=65,h=80,p=50,v=!1,g=!1,x=!1,w=!1,y="none",L=!1,b=!1,E=!1,T=!1;function O(){var e=document.body;if(e){u=void 0!==window.BX?"bitrix":void 0!==window.B24||void 0!==window.BX24?"bitrix24":document.querySelector(".t-records")||void 0!==window.Tilda?"tilda":null;var t=e.dataset;if(v="prefetchAllowQueryString"in t||"instantAllowQueryString"in t,g="prefetchAllowExternalLinks"in t||"instantAllowExternalLinks"in t,x="prefetchWhitelist"in t||"instantWhitelist"in t,t.prefetchNonce&&(d=t.prefetchNonce),!d&&t.instantNonce&&(d=t.instantNonce),!a&&("prefetchSpecrules"in t||"instantSpecrules"in t)&&HTMLScriptElement.supports&&HTMLScriptElement.supports("speculationrules")){var n=t.prefetchSpecrules||t.instantSpecrules;"prerender"===n?(y="prerender",w=!0):"no"!==n&&(y="prefetch",w=!0)}L="prefetchPrerenderAll"in t||"instantPrerenderAll"in t;var r=t.prefetchIntensity||t.instantIntensity;if("mousedown"===r)b=!0;else if("viewport"===r||"viewport-all"===r)("viewport-all"===r||o&&S())&&(E=!0);else if(r){var i=parseInt(r,10);!isNaN(i)&&i>=0&&(m=i)}o&&(h=Math.max(60,Math.min(m||0,150))),(T="prefetchObserveDom"in t)||"bitrix"!==u&&"tilda"!==u||(T=!0),"tilda"===u&&m<100&&(m=100);var c={capture:!0,passive:!0};if(document.addEventListener("touchstart",k,c),b?document.addEventListener("mousedown",M,c):document.addEventListener("mouseover",N,c),E)(window.requestIdleCallback||function(e){setTimeout(e,1)})(R,{timeout:1500});T&&function(){if(z)return;(z=new MutationObserver(function(e){e.some(function(e){return Array.from(e.addedNodes).some(function(e){return 1===e.nodeType&&("A"===e.tagName||e.querySelectorAll&&e.querySelectorAll("a").length)})})&&B&&(clearTimeout(H),H=setTimeout(U,100))})).observe(document.body,{childList:!0,subtree:!0})}()}}function S(){return!s&&("slow-2g"!==l&&"2g"!==l)}function A(e){return e?(e.nodeType&&1!==e.nodeType&&(e=e.parentElement),e&&"function"==typeof e.closest?e.closest("a"):null):null}function k(e){n=e.timeStamp||Date.now();var t=A(e.target);if(C(t)){r&&(clearTimeout(r),r=0),i&&(document.removeEventListener("touchmove",i,!0),document.removeEventListener("scroll",i,!0),i=null);var o=!1;i=function(){o=!0,r&&(clearTimeout(r),r=0),document.removeEventListener("touchmove",i,!0),document.removeEventListener("scroll",i,!0),i=null},document.addEventListener("touchmove",i,{capture:!0,passive:!0,once:!0}),document.addEventListener("scroll",i,{capture:!0,passive:!0,once:!0}),r=setTimeout(function(){i&&(document.removeEventListener("touchmove",i,!0),document.removeEventListener("scroll",i,!0),i=null),r=0,o||W(t.href,t)},h)}}function N(e){if(!(n&&e.timeStamp&&e.timeStamp-n<2500)){var r=A(e.target);if(C(r)){r.addEventListener("mouseout",P,{passive:!0,once:!0});var i=setTimeout(function(){W(r.href,r),t.delete(r)},m);t.set(r,i)}}}function P(e){var n=A(e.target);if(n&&(!e.relatedTarget||!e.relatedTarget.closest||n!==e.relatedTarget.closest("a"))){var r=t.get(n);r&&(clearTimeout(r),t.delete(n))}}function M(e){if(!("number"==typeof e.button&&2===e.button||n&&e.timeStamp&&e.timeStamp-n<2500)){var t=A(e.target);C(t)&&W(t.href,t)}}function C(t){if(!t||!t.href)return!1;if(t.target&&"_self"!==t.target)return!1;if(t.hasAttribute("download"))return!1;if("noPrefetch"in t.dataset||"prefetchNo"in t.dataset)return!1;if(x&&!("prefetch"in t.dataset)&&!("instant"in t.dataset))return!1;if("http:"!==t.protocol&&"https:"!==t.protocol)return!1;if("http:"===t.protocol&&"https:"===location.protocol)return!1;if(t.origin!==location.origin){if(!g&&!("prefetch"in t.dataset)&&!("instant"in t.dataset))return!1;if(!c)return!1}if(t.search&&!v&&!("prefetch"in t.dataset)&&!("instant"in t.dataset))return!1;if(t.hash&&t.pathname+t.search===location.pathname+location.search)return!1;var n=I(t.href);return!e.has(n)&&(!!function(e){var t=e.href;if("bitrix"===u||"bitrix24"===u){if(-1!==t.indexOf("/bitrix/")||-1!==t.indexOf("sessid="))return!1;if(e.classList.contains("bx-ajax"))return!1}if("tilda"===u&&(-1!==t.indexOf("#popup:")||-1!==t.indexOf("#rec")))return!1;return-1===t.indexOf("/login")&&-1===t.indexOf("/logout")&&-1===t.indexOf("/auth")&&-1===t.indexOf("/register")&&-1===t.indexOf("/cart")&&-1===t.indexOf("/basket")&&-1===t.indexOf("/add")&&-1===t.indexOf("/delete")&&-1===t.indexOf("/remove")&&!/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)/.test(t)}(t)&&!!function(e){var t=e.href,n=e.className||"",r="";try{r=new URL(t,location.href).hostname}catch(e){r=""}return-1===n.indexOf("ym-")&&("mc.yandex.ru"!==r&&"metrika.yandex.ru"!==r&&(-1===n.indexOf("ga-")&&-1===n.indexOf("gtm-")&&("google-analytics.com"!==r&&!r.endsWith(".google-analytics.com")&&("googletagmanager.com"!==r&&!r.endsWith(".googletagmanager.com")&&(-1===n.indexOf("piwik")&&-1===n.indexOf("matomo")&&("matomo.org"!==r&&!r.endsWith(".matomo.org")&&"piwik.org"!==r&&!r.endsWith(".piwik.org")))))))}(t))}function I(e){try{var t=new URL(e,location.href);return t.origin+t.pathname+t.search}catch(t){return e}}function W(t,n){if(S()){var r=function(e){try{var t=new URL(e,location.href);return t.hash="",t.href}catch(t){return e}}(t),i=I(r);if(!e.has(i)){e.size>=p&&e.delete(e.values().next().value),e.add(i);var o=function(e){return w&&"none"!==y?"prerender"!==y?y:L||x||e&&e.dataset&&("prefetchPrerender"in e.dataset||"instantPrerender"in e.dataset)?"prerender":"prefetch":"none"}(n);if("none"!==o)return function(e,t){var n=document.head;if(!n)return;var r=document.createElement("script");r.type="speculationrules",d&&(r.nonce=d);var i={};i[t]=[{source:"list",urls:[e]}],r.textContent=JSON.stringify(i),n.appendChild(r)}(r,o),void(a||!f?D(r,i):q(r,i));a||!f?D(r,i):q(r,i)}}}function q(t,n){var r=document.head;if(r){var i=document.createElement("link");i.rel="prefetch",i.href=t,i.as="document";try{i.fetchPriority="low"}catch(e){}i.onerror=function(){e.delete(n)},r.appendChild(i)}}function D(t,n){if("function"==typeof fetch){var r=null,i=0;"undefined"!=typeof AbortController&&(r=new AbortController,i=setTimeout(function(){try{r.abort()}catch(e){}},5e3));var o={method:"GET",credentials:"same-origin",cache:"force-cache",headers:{Purpose:"prefetch"}};r&&(o.signal=r.signal);try{fetch(t,o).then(function(t){i&&clearTimeout(i),t&&t.ok||e.delete(n)}).catch(function(){i&&clearTimeout(i),e.delete(n)})}catch(t){i&&clearTimeout(i),e.delete(n)}}}!function(){try{var e=document.currentScript;e&&e.nonce&&(d=e.nonce)}catch(e){}try{var t=document.createElement("link");t.relList&&"function"==typeof t.relList.supports&&(f=t.relList.supports("prefetch"))}catch(e){}var n=navigator.userAgent;a=/iPad|iPhone/.test(n)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1;var r=/Android/.test(n);(o=(a||r)&&Math.min(screen.width,screen.height)<768)&&(p=20);var i=n.match(/Chrome\/(\d+)/);i&&(c=parseInt(i[1],10));var u=navigator.connection;u&&(l=u.effectiveType,s=u.saveData||!1),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",O):O()}();var B=null;function R(){B||(B=new IntersectionObserver(function(e){e.forEach(function(e){e.isIntersecting&&(B.unobserve(e.target),C(e.target)&&W(e.target.href,e.target))})},{rootMargin:o?"100px":"200px"}),U())}function U(){B&&document.querySelectorAll("a").forEach(function(e){C(e)&&B.observe(e)})}var z=null,H=null;window.Prefetch={version:"1.0.6",preload:function(e){W(e)}}}();
6
+ !function(){"use strict";var e=new Set,t=new WeakMap,n=0,r=0,i=null,o=!1,a=!1,c=null,u=null,s=!1,l=null,f=null,d=!1,h=65,m=80,p=50,v=!1,g=!1,w=!1,x=!1,y="none",L=!1,b=!1,E=!1,T=!1;function S(){var e=document.body;if(e){u=void 0!==window.BX?"bitrix":void 0!==window.B24||void 0!==window.BX24?"bitrix24":document.querySelector(".t-records")||void 0!==window.Tilda?"tilda":null;var t=e.dataset;if(v="prefetchAllowQueryString"in t||"instantAllowQueryString"in t,g="prefetchAllowExternalLinks"in t||"instantAllowExternalLinks"in t,w="prefetchWhitelist"in t||"instantWhitelist"in t,t.prefetchNonce&&(f=t.prefetchNonce),!f&&t.instantNonce&&(f=t.instantNonce),!a&&("prefetchSpecrules"in t||"instantSpecrules"in t)&&HTMLScriptElement.supports&&HTMLScriptElement.supports("speculationrules")){var n=t.prefetchSpecrules||t.instantSpecrules;"prerender"===n?(y="prerender",x=!0):"no"!==n&&(y="prefetch",x=!0)}L="prefetchPrerenderAll"in t||"instantPrerenderAll"in t;var r=t.prefetchIntensity||t.instantIntensity;if("mousedown"===r)b=!0;else if("viewport"===r||"viewport-all"===r)("viewport-all"===r||o&&O())&&(E=!0);else if(r){var i=parseInt(r,10);!isNaN(i)&&i>=0&&(h=i)}o&&(m=Math.max(60,Math.min(h||0,150))),(T="prefetchObserveDom"in t)||"bitrix"!==u&&"tilda"!==u||(T=!0),"tilda"===u&&h<100&&(h=100);var c={capture:!0,passive:!0};if(document.addEventListener("touchstart",k,c),b?document.addEventListener("mousedown",M,c):document.addEventListener("mouseover",N,c),E)(window.requestIdleCallback||function(e){setTimeout(e,1)})(U,{timeout:1500});T&&function(){if(z)return;(z=new MutationObserver(function(e){e.some(function(e){return Array.from(e.addedNodes).some(function(e){return 1===e.nodeType&&("A"===e.tagName||e.querySelectorAll&&e.querySelectorAll("a").length)})})&&R&&(clearTimeout(H),H=setTimeout(B,100))})).observe(document.body,{childList:!0,subtree:!0})}()}}function O(){return!s&&("slow-2g"!==l&&"2g"!==l)}function A(e){return e?(e.nodeType&&1!==e.nodeType&&(e=e.parentElement),e&&"function"==typeof e.closest?e.closest("a"):null):null}function k(e){n=e.timeStamp||Date.now();var t=A(e.target);if(C(t)){r&&(clearTimeout(r),r=0),i&&(document.removeEventListener("touchmove",i,!0),document.removeEventListener("scroll",i,!0),i=null);var o=!1;i=function(){o=!0,r&&(clearTimeout(r),r=0),document.removeEventListener("touchmove",i,!0),document.removeEventListener("scroll",i,!0),i=null},document.addEventListener("touchmove",i,{capture:!0,passive:!0,once:!0}),document.addEventListener("scroll",i,{capture:!0,passive:!0,once:!0}),r=setTimeout(function(){i&&(document.removeEventListener("touchmove",i,!0),document.removeEventListener("scroll",i,!0),i=null),r=0,o||W(t.href,t)},m)}}function N(e){if(!(n&&e.timeStamp&&e.timeStamp-n<2500)){var r=A(e.target);if(C(r)&&!t.has(r)){r.addEventListener("mouseleave",P,{passive:!0,once:!0});var i=setTimeout(function(){W(r.href,r),t.delete(r)},h);t.set(r,i)}}}function P(e){var n=e.currentTarget;if(n){var r=t.get(n);r&&(clearTimeout(r),t.delete(n))}}function M(e){if(!("number"==typeof e.button&&2===e.button||n&&e.timeStamp&&e.timeStamp-n<2500)){var t=A(e.target);C(t)&&W(t.href,t)}}function C(t){if(!t)return!1;var n=t.getAttribute("href");if(null===n||""===n.trim())return!1;if(!t.href)return!1;if(t.target&&"_self"!==t.target)return!1;if(t.hasAttribute("download"))return!1;if("noPrefetch"in t.dataset||"prefetchNo"in t.dataset)return!1;if(w&&!("prefetch"in t.dataset)&&!("instant"in t.dataset))return!1;if("http:"!==t.protocol&&"https:"!==t.protocol)return!1;if("http:"===t.protocol&&"https:"===location.protocol)return!1;if(t.origin!==location.origin){if(!g&&!("prefetch"in t.dataset)&&!("instant"in t.dataset))return!1;if(!c)return!1}if(t.search&&!v&&!("prefetch"in t.dataset)&&!("instant"in t.dataset))return!1;if(t.hash&&t.pathname+t.search===location.pathname+location.search)return!1;if(I(t.href)===I(location.href))return!1;var r=I(t.href);return!e.has(r)&&(!!function(e){var t=e.href,n="",r="";try{var i=new URL(t,location.href);n=i.pathname||"",r=i.hash||""}catch(e){n="",r=""}if("bitrix"===u||"bitrix24"===u){if(-1!==t.indexOf("/bitrix/")||-1!==t.indexOf("sessid="))return!1;if(e.classList.contains("bx-ajax"))return!1}if("tilda"===u&&(-1!==r.indexOf("#popup:")||-1!==r.indexOf("#rec")))return!1;var o=/(^|\/)(add|delete|remove)(\/|$|\.)/i.test(n);return-1===t.indexOf("/login")&&-1===t.indexOf("/logout")&&-1===t.indexOf("/auth")&&-1===t.indexOf("/register")&&-1===t.indexOf("/cart")&&-1===t.indexOf("/basket")&&!o&&!/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)/i.test(t)}(t)&&!!function(e){var t=e.href,n=e.className||"",r="";try{r=new URL(t,location.href).hostname}catch(e){r=""}return-1===n.indexOf("ym-")&&("mc.yandex.ru"!==r&&"metrika.yandex.ru"!==r&&(-1===n.indexOf("ga-")&&-1===n.indexOf("gtm-")&&("google-analytics.com"!==r&&!r.endsWith(".google-analytics.com")&&("googletagmanager.com"!==r&&!r.endsWith(".googletagmanager.com")&&(-1===n.indexOf("piwik")&&-1===n.indexOf("matomo")&&("matomo.org"!==r&&!r.endsWith(".matomo.org")&&"piwik.org"!==r&&!r.endsWith(".piwik.org")))))))}(t))}function I(e){try{var t=new URL(e,location.href);return t.origin+t.pathname+t.search}catch(t){return e}}function W(t,n){if(O()){var r=function(e){try{var t=new URL(e,location.href);return t.hash="",t.href}catch(t){return e}}(t),i=I(r);if(!e.has(i)){e.size>=p&&e.delete(e.values().next().value),e.add(i);var o=function(e){return x&&"none"!==y?"prerender"!==y?y:L||w||e&&e.dataset&&("prefetchPrerender"in e.dataset||"instantPrerender"in e.dataset)?"prerender":"prefetch":"none"}(n);if("none"!==o)return function(e,t){var n=document.head;if(!n)return;var r=document.createElement("script");r.type="speculationrules",f&&(r.nonce=f);var i={};i[t]=[{source:"list",urls:[e]}],r.textContent=JSON.stringify(i),n.appendChild(r)}(r,o),void(a||!d?D(r,i):q(r,i));a||!d?D(r,i):q(r,i)}}}function q(t,n){var r=document.head;if(r){var i=document.createElement("link");i.rel="prefetch",i.href=t,i.as="document";try{i.fetchPriority="low"}catch(e){}i.onerror=function(){e.delete(n)},r.appendChild(i)}}function D(t,n){if("function"==typeof fetch){var r=null,i=0;"undefined"!=typeof AbortController&&(r=new AbortController,i=setTimeout(function(){try{r.abort()}catch(e){}},5e3));var o={method:"GET",credentials:"same-origin",cache:"force-cache",headers:{Purpose:"prefetch"}};r&&(o.signal=r.signal);try{fetch(t,o).then(function(t){i&&clearTimeout(i),t&&t.ok||e.delete(n)}).catch(function(){i&&clearTimeout(i),e.delete(n)})}catch(t){i&&clearTimeout(i),e.delete(n)}}}!function(){try{var e=document.currentScript;e&&e.nonce&&(f=e.nonce)}catch(e){}try{var t=document.createElement("link");t.relList&&"function"==typeof t.relList.supports&&(d=t.relList.supports("prefetch"))}catch(e){}var n=navigator.userAgent;a=/iPad|iPhone/.test(n)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1;var r=/Android/.test(n);(o=(a||r)&&Math.min(screen.width,screen.height)<768)&&(p=20);var i=n.match(/Chrome\/(\d+)/);i&&(c=parseInt(i[1],10));var u=navigator.connection;u&&(l=u.effectiveType,s=u.saveData||!1),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",S):S()}();var R=null;function U(){R||(R=new IntersectionObserver(function(e){e.forEach(function(e){e.isIntersecting&&(R.unobserve(e.target),C(e.target)&&W(e.target.href,e.target))})},{rootMargin:o?"100px":"200px"}),B())}function B(){R&&document.querySelectorAll("a").forEach(function(e){C(e)&&R.observe(e)})}var z=null,H=null;window.Prefetch={version:"1.0.8",preload:function(e){W(e)}}}();
package/package.json CHANGED
@@ -1,15 +1,27 @@
1
1
  {
2
2
  "name": "@prefetchru/prefetch",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Мгновенная загрузка страниц для российского веба. Instant page loading for Russian web.",
5
5
  "main": "prefetch.js",
6
- "module": "prefetch.js",
6
+ "module": "prefetch.esm.js",
7
7
  "browser": "dist/prefetch.min.js",
8
8
  "unpkg": "dist/prefetch.min.js",
9
9
  "jsdelivr": "dist/prefetch.min.js",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./prefetch.esm.js",
13
+ "require": "./prefetch.js",
14
+ "browser": "./dist/prefetch.min.js"
15
+ },
16
+ "./esm": "./prefetch.esm.js",
17
+ "./esm/min": "./dist/prefetch.esm.min.js"
18
+ },
10
19
  "files": [
11
20
  "prefetch.js",
12
- "dist/prefetch.min.js"
21
+ "prefetch.esm.js",
22
+ "dist/prefetch.min.js",
23
+ "dist/prefetch.esm.min.js",
24
+ "README.md"
13
25
  ],
14
26
  "scripts": {
15
27
  "minify": "node build.js",
@@ -0,0 +1,614 @@
1
+ /*!
2
+ * prefetch.ru v1.0.8 (ESM) - Мгновенная загрузка страниц
3
+ * © 2026 Сергей Макаров | MIT License
4
+ * https://prefetch.ru | https://github.com/prefetch-ru
5
+ */
6
+ function createPrefetch(importMetaUrl) {
7
+ 'use strict'
8
+
9
+ // SSR/Non-browser guard
10
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
11
+ return { version: '1.0.8', preload: function () {} }
12
+ }
13
+
14
+ // Состояние
15
+ var preloaded = new Set()
16
+ var hoverTimers = new WeakMap()
17
+
18
+ var lastTouchTime = 0
19
+ var touchTimer = 0
20
+ var touchCancel = null
21
+
22
+ var isMobile = false
23
+ var isIOS = false
24
+ var chromiumVer = null
25
+ var platform = null
26
+ var saveData = false
27
+ var connType = null
28
+
29
+ // CSP / поддержка
30
+ var scriptNonce = null
31
+ var supportsLinkPrefetch = false
32
+
33
+ // Настройки
34
+ var hoverDelay = 65
35
+ var touchDelay = 80
36
+ var maxPreloads = 50
37
+ var allowQuery = false
38
+ var allowExternal = false
39
+ var whitelist = false
40
+
41
+ var useSpecRules = false
42
+ var specMode = 'none' // 'none' | 'prefetch' | 'prerender'
43
+ var prerenderAll = false
44
+
45
+ var mousedownMode = false
46
+ var viewportMode = false
47
+ var observeDom = false
48
+
49
+ function detectNonceFromImportMeta(metaUrl) {
50
+ try {
51
+ if (!metaUrl) return null
52
+ // script.src и import.meta.url обычно оба абсолютные → можно сравнивать напрямую
53
+ var scripts = document.getElementsByTagName('script')
54
+ for (var i = 0; i < scripts.length; i++) {
55
+ var s = scripts[i]
56
+ if (!s || !s.src) continue
57
+ if (s.src === metaUrl) {
58
+ var n = s.nonce || s.getAttribute('nonce') || null
59
+ if (n) return n
60
+ break
61
+ }
62
+ }
63
+ } catch (e) {}
64
+ return null
65
+ }
66
+
67
+ // Инициализация
68
+ ;(function init() {
69
+ // CSP nonce (ESM): через import.meta.url → находим <script type="module" src="..."> и читаем nonce
70
+ scriptNonce = detectNonceFromImportMeta(importMetaUrl)
71
+
72
+ // fallback: на случай окружений, где currentScript всё же доступен
73
+ if (!scriptNonce) {
74
+ try {
75
+ var cs = document.currentScript
76
+ if (cs && cs.nonce) scriptNonce = cs.nonce
77
+ } catch (e) {}
78
+ }
79
+
80
+ // rel=prefetch support
81
+ try {
82
+ var l = document.createElement('link')
83
+ if (l.relList && typeof l.relList.supports === 'function') {
84
+ supportsLinkPrefetch = l.relList.supports('prefetch')
85
+ }
86
+ } catch (e) {}
87
+
88
+ var ua = navigator.userAgent
89
+
90
+ // Определяем устройство
91
+ isIOS =
92
+ /iPad|iPhone/.test(ua) ||
93
+ (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
94
+ var isAndroid = /Android/.test(ua)
95
+ isMobile = (isIOS || isAndroid) && Math.min(screen.width, screen.height) < 768
96
+ if (isMobile) maxPreloads = 20
97
+
98
+ // Chromium версия (для внешних ссылок)
99
+ var cm = ua.match(/Chrome\/(\d+)/)
100
+ if (cm) chromiumVer = parseInt(cm[1], 10)
101
+
102
+ // Сеть
103
+ var conn = navigator.connection
104
+ if (conn) {
105
+ connType = conn.effectiveType
106
+ saveData = conn.saveData || false
107
+ }
108
+
109
+ // Ждём DOM
110
+ if (document.readyState === 'loading') {
111
+ document.addEventListener('DOMContentLoaded', setup)
112
+ } else {
113
+ setup()
114
+ }
115
+ })()
116
+
117
+ function setup() {
118
+ var body = document.body
119
+ if (!body) return
120
+
121
+ platform = detectPlatform()
122
+
123
+ // Читаем конфигурацию
124
+ var ds = body.dataset
125
+ allowQuery = 'prefetchAllowQueryString' in ds || 'instantAllowQueryString' in ds
126
+ allowExternal = 'prefetchAllowExternalLinks' in ds || 'instantAllowExternalLinks' in ds
127
+ whitelist = 'prefetchWhitelist' in ds || 'instantWhitelist' in ds
128
+
129
+ // CSP nonce override через data-*
130
+ // <body data-prefetch-nonce="...">
131
+ if (ds.prefetchNonce) scriptNonce = ds.prefetchNonce
132
+ if (!scriptNonce && ds.instantNonce) scriptNonce = ds.instantNonce
133
+
134
+ // Speculation Rules — opt-in по наличию атрибута:
135
+ // <body data-prefetch-specrules> (prefetch)
136
+ // <body data-prefetch-specrules="prerender">
137
+ // <body data-prefetch-specrules="no">
138
+ var hasSr = 'prefetchSpecrules' in ds || 'instantSpecrules' in ds
139
+ if (
140
+ !isIOS &&
141
+ hasSr &&
142
+ HTMLScriptElement.supports &&
143
+ HTMLScriptElement.supports('speculationrules')
144
+ ) {
145
+ var sr = ds.prefetchSpecrules || ds.instantSpecrules
146
+ if (sr === 'prerender') {
147
+ specMode = 'prerender'
148
+ useSpecRules = true
149
+ } else if (sr !== 'no') {
150
+ specMode = 'prefetch'
151
+ useSpecRules = true
152
+ }
153
+ }
154
+
155
+ // Разрешить "глобальный" prerender без whitelist (не рекомендуется, но бывает нужно)
156
+ // <body data-prefetch-prerender-all>
157
+ prerenderAll = 'prefetchPrerenderAll' in ds || 'instantPrerenderAll' in ds
158
+
159
+ // Интенсивность
160
+ var intensity = ds.prefetchIntensity || ds.instantIntensity
161
+ if (intensity === 'mousedown') {
162
+ mousedownMode = true
163
+ } else if (intensity === 'viewport' || intensity === 'viewport-all') {
164
+ if (intensity === 'viewport-all' || (isMobile && isNetworkOk())) {
165
+ viewportMode = true
166
+ }
167
+ } else if (intensity) {
168
+ var d = parseInt(intensity, 10)
169
+ if (!isNaN(d) && d >= 0) hoverDelay = d
170
+ }
171
+
172
+ // На мобильных делаем touch-предзагрузку менее агрессивной
173
+ if (isMobile) {
174
+ touchDelay = Math.max(60, Math.min(hoverDelay || 0, 150))
175
+ }
176
+
177
+ // DOM observer для SPA
178
+ observeDom = 'prefetchObserveDom' in ds
179
+ if (!observeDom && (platform === 'bitrix' || platform === 'tilda')) {
180
+ observeDom = true
181
+ }
182
+
183
+ // Tilda — увеличиваем задержку из-за popup-ов
184
+ if (platform === 'tilda' && hoverDelay < 100) hoverDelay = 100
185
+
186
+ // События
187
+ var opts = { capture: true, passive: true }
188
+ document.addEventListener('touchstart', onTouchStart, opts)
189
+ if (!mousedownMode) {
190
+ document.addEventListener('mouseover', onMouseOver, opts)
191
+ } else {
192
+ document.addEventListener('mousedown', onMouseDown, opts)
193
+ }
194
+
195
+ // Viewport observer
196
+ if (viewportMode) {
197
+ var rIC = window.requestIdleCallback || function (cb) { setTimeout(cb, 1) }
198
+ rIC(startViewportObserver, { timeout: 1500 })
199
+ }
200
+
201
+ // Mutation observer
202
+ if (observeDom) startMutationObserver()
203
+ }
204
+
205
+ function detectPlatform() {
206
+ if (typeof window.BX !== 'undefined') return 'bitrix'
207
+ if (typeof window.B24 !== 'undefined' || typeof window.BX24 !== 'undefined') return 'bitrix24'
208
+ if (document.querySelector('.t-records') || typeof window.Tilda !== 'undefined') return 'tilda'
209
+ return null
210
+ }
211
+
212
+ function isNetworkOk() {
213
+ if (saveData) return false
214
+ if (connType === 'slow-2g' || connType === '2g') return false
215
+ return true
216
+ }
217
+
218
+ function getAnchorFromEventTarget(t) {
219
+ if (!t) return null
220
+ if (t.nodeType && t.nodeType !== 1) t = t.parentElement
221
+ if (!t || typeof t.closest !== 'function') return null
222
+ return t.closest('a')
223
+ }
224
+
225
+ function onTouchStart(e) {
226
+ lastTouchTime = e.timeStamp || Date.now()
227
+
228
+ var a = getAnchorFromEventTarget(e.target)
229
+ if (!canPreload(a)) return
230
+
231
+ // задержка + отмена на scroll/touchmove
232
+ if (touchTimer) {
233
+ clearTimeout(touchTimer)
234
+ touchTimer = 0
235
+ }
236
+ if (touchCancel) {
237
+ document.removeEventListener('touchmove', touchCancel, true)
238
+ document.removeEventListener('scroll', touchCancel, true)
239
+ touchCancel = null
240
+ }
241
+
242
+ var cancelled = false
243
+ touchCancel = function () {
244
+ cancelled = true
245
+ if (touchTimer) {
246
+ clearTimeout(touchTimer)
247
+ touchTimer = 0
248
+ }
249
+ document.removeEventListener('touchmove', touchCancel, true)
250
+ document.removeEventListener('scroll', touchCancel, true)
251
+ touchCancel = null
252
+ }
253
+
254
+ document.addEventListener('touchmove', touchCancel, { capture: true, passive: true, once: true })
255
+ document.addEventListener('scroll', touchCancel, { capture: true, passive: true, once: true })
256
+
257
+ touchTimer = setTimeout(function () {
258
+ if (touchCancel) {
259
+ document.removeEventListener('touchmove', touchCancel, true)
260
+ document.removeEventListener('scroll', touchCancel, true)
261
+ touchCancel = null
262
+ }
263
+ touchTimer = 0
264
+ if (!cancelled) preload(a.href, a)
265
+ }, touchDelay)
266
+ }
267
+
268
+ function onMouseOver(e) {
269
+ if (lastTouchTime && e.timeStamp && e.timeStamp - lastTouchTime < 2500) return
270
+
271
+ var a = getAnchorFromEventTarget(e.target)
272
+ if (!canPreload(a)) return
273
+
274
+ // v1.0.7: защита от множественных mouseover по вложенным элементам (не плодим таймеры)
275
+ if (hoverTimers.has(a)) return
276
+
277
+ // mouseleave не срабатывает при перемещении внутри ссылки (в отличие от mouseout)
278
+ a.addEventListener('mouseleave', onMouseLeave, { passive: true, once: true })
279
+
280
+ var t = setTimeout(function () {
281
+ preload(a.href, a)
282
+ hoverTimers.delete(a)
283
+ }, hoverDelay)
284
+ hoverTimers.set(a, t)
285
+ }
286
+
287
+ function onMouseLeave(e) {
288
+ var a = e.currentTarget
289
+ if (!a) return
290
+
291
+ var t = hoverTimers.get(a)
292
+ if (t) {
293
+ clearTimeout(t)
294
+ hoverTimers.delete(a)
295
+ }
296
+ }
297
+
298
+ function onMouseDown(e) {
299
+ if (typeof e.button === 'number' && e.button === 2) return
300
+ if (lastTouchTime && e.timeStamp && e.timeStamp - lastTouchTime < 2500) return
301
+
302
+ var a = getAnchorFromEventTarget(e.target)
303
+ if (canPreload(a)) preload(a.href, a)
304
+ }
305
+
306
+ function canPreload(a) {
307
+ if (!a) return false
308
+
309
+ // v1.0.7: исключаем <a href=""> и <a> без href (часто используются как кнопки)
310
+ var hrefAttr = a.getAttribute('href')
311
+ if (hrefAttr === null || hrefAttr.trim() === '') return false
312
+
313
+ if (!a.href) return false
314
+
315
+ // Не навигация в текущей вкладке
316
+ if (a.target && a.target !== '_self') return false
317
+ if (a.hasAttribute('download')) return false
318
+
319
+ // Явный запрет
320
+ if ('noPrefetch' in a.dataset || 'prefetchNo' in a.dataset) return false
321
+
322
+ // Белый список
323
+ if (whitelist && !('prefetch' in a.dataset) && !('instant' in a.dataset)) return false
324
+
325
+ // Протокол
326
+ if (a.protocol !== 'http:' && a.protocol !== 'https:') return false
327
+ if (a.protocol === 'http:' && location.protocol === 'https:') return false
328
+
329
+ // Внешние ссылки
330
+ if (a.origin !== location.origin) {
331
+ if (!allowExternal && !('prefetch' in a.dataset) && !('instant' in a.dataset)) return false
332
+ if (!chromiumVer) return false
333
+ }
334
+
335
+ // Query string
336
+ if (a.search && !allowQuery && !('prefetch' in a.dataset) && !('instant' in a.dataset)) return false
337
+
338
+ // Якорь на той же странице
339
+ if (a.hash && a.pathname + a.search === location.pathname + location.search) return false
340
+
341
+ // v1.0.7: не префетчим текущую страницу (в т.ч. для случаев вроде href="")
342
+ if (urlKey(a.href) === urlKey(location.href)) return false
343
+
344
+ // Уже загружено (ключ НЕ модифицирует реальный URL запроса!)
345
+ var key = urlKey(a.href)
346
+ if (preloaded.has(key)) return false
347
+
348
+ if (!checkPlatform(a)) return false
349
+ if (!checkAnalytics(a)) return false
350
+
351
+ return true
352
+ }
353
+
354
+ function checkPlatform(a) {
355
+ var href = a.href
356
+
357
+ // v1.0.7: для точных проверок /add /delete /remove используем pathname
358
+ var pathname = ''
359
+ var hash = ''
360
+ try {
361
+ var u = new URL(href, location.href)
362
+ pathname = u.pathname || ''
363
+ hash = u.hash || ''
364
+ } catch (e) {
365
+ pathname = ''
366
+ hash = ''
367
+ }
368
+
369
+ if (platform === 'bitrix' || platform === 'bitrix24') {
370
+ if (href.indexOf('/bitrix/') !== -1 || href.indexOf('sessid=') !== -1) return false
371
+ if (a.classList.contains('bx-ajax')) return false
372
+ }
373
+
374
+ if (platform === 'tilda') {
375
+ if (hash.indexOf('#popup:') !== -1 || hash.indexOf('#rec') !== -1) return false
376
+ }
377
+
378
+ // v1.0.7: /add /delete /remove — только как отдельный сегмент пути (или имя файла типа /delete.php)
379
+ var isActionPath = /(^|\/)(add|delete|remove)(\/|$|\.)/i.test(pathname)
380
+
381
+ if (
382
+ href.indexOf('/login') !== -1 ||
383
+ href.indexOf('/logout') !== -1 ||
384
+ href.indexOf('/auth') !== -1 ||
385
+ href.indexOf('/register') !== -1 ||
386
+ href.indexOf('/cart') !== -1 ||
387
+ href.indexOf('/basket') !== -1 ||
388
+ isActionPath
389
+ ) return false
390
+
391
+ if (/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)/i.test(href)) return false
392
+
393
+ return true
394
+ }
395
+
396
+ function checkAnalytics(a) {
397
+ var href = a.href
398
+ var cls = a.className || ''
399
+
400
+ // Извлекаем hostname для проверки доменов аналитики
401
+ var host = ''
402
+ try { host = new URL(href, location.href).hostname } catch (e) { host = '' }
403
+
404
+ if (cls.indexOf('ym-') !== -1) return false
405
+ if (host === 'mc.yandex.ru' || host === 'metrika.yandex.ru') return false
406
+
407
+ if (cls.indexOf('ga-') !== -1 || cls.indexOf('gtm-') !== -1) return false
408
+ if (host === 'google-analytics.com' || host.endsWith('.google-analytics.com')) return false
409
+ if (host === 'googletagmanager.com' || host.endsWith('.googletagmanager.com')) return false
410
+
411
+ if (cls.indexOf('piwik') !== -1 || cls.indexOf('matomo') !== -1) return false
412
+ if (
413
+ host === 'matomo.org' ||
414
+ host.endsWith('.matomo.org') ||
415
+ host === 'piwik.org' ||
416
+ host.endsWith('.piwik.org')
417
+ ) return false
418
+
419
+ return true
420
+ }
421
+
422
+ // Ключ для дедупликации: НЕ трогаем pathname (включая / на конце), только убираем hash
423
+ function urlKey(url) {
424
+ try {
425
+ var u = new URL(url, location.href)
426
+ return u.origin + u.pathname + u.search
427
+ } catch (e) {
428
+ return url
429
+ }
430
+ }
431
+
432
+ // URL для реального запроса: абсолютный, без hash, без "улучшений"
433
+ function urlForRequest(url) {
434
+ try {
435
+ var u = new URL(url, location.href)
436
+ u.hash = ''
437
+ return u.href
438
+ } catch (e) {
439
+ return url
440
+ }
441
+ }
442
+
443
+ function resolveSpecMode(a) {
444
+ if (!useSpecRules || specMode === 'none') return 'none'
445
+ if (specMode !== 'prerender') return specMode
446
+
447
+ // specMode === 'prerender'
448
+ if (prerenderAll) return 'prerender'
449
+ if (whitelist) return 'prerender' // ссылки и так явно размечены
450
+
451
+ // иначе prerender только по ссылке:
452
+ if (a && a.dataset && (('prefetchPrerender' in a.dataset) || ('instantPrerender' in a.dataset))) {
453
+ return 'prerender'
454
+ }
455
+ return 'prefetch'
456
+ }
457
+
458
+ function preload(url, a) {
459
+ if (!isNetworkOk()) return
460
+
461
+ var requestUrl = urlForRequest(url)
462
+ var key = urlKey(requestUrl)
463
+
464
+ if (preloaded.has(key)) return
465
+ if (preloaded.size >= maxPreloads) {
466
+ preloaded.delete(preloaded.values().next().value)
467
+ }
468
+ preloaded.add(key)
469
+
470
+ var mode = resolveSpecMode(a)
471
+
472
+ if (mode !== 'none') {
473
+ preloadSpec(requestUrl, mode)
474
+
475
+ // Страховка: обычный prefetch (чтобы под CSP/ограничениями SpecRules всё равно грелся кэш)
476
+ if (isIOS || !supportsLinkPrefetch) preloadFetch(requestUrl, key)
477
+ else preloadLink(requestUrl, key)
478
+ return
479
+ }
480
+
481
+ if (isIOS || !supportsLinkPrefetch) preloadFetch(requestUrl, key)
482
+ else preloadLink(requestUrl, key)
483
+ }
484
+
485
+ function preloadSpec(url, mode) {
486
+ var head = document.head
487
+ if (!head) return
488
+
489
+ var s = document.createElement('script')
490
+ s.type = 'speculationrules'
491
+ if (scriptNonce) s.nonce = scriptNonce
492
+
493
+ var rules = {}
494
+ rules[mode] = [{ source: 'list', urls: [url] }]
495
+ s.textContent = JSON.stringify(rules)
496
+ head.appendChild(s)
497
+ }
498
+
499
+ function preloadLink(url, key) {
500
+ var head = document.head
501
+ if (!head) return
502
+
503
+ var l = document.createElement('link')
504
+ l.rel = 'prefetch'
505
+ l.href = url
506
+ l.as = 'document'
507
+ try { l.fetchPriority = 'low' } catch (e) {}
508
+
509
+ l.onerror = function () { preloaded.delete(key) }
510
+ head.appendChild(l)
511
+ }
512
+
513
+ function preloadFetch(url, key) {
514
+ if (typeof fetch !== 'function') return
515
+
516
+ var ctrl = null
517
+ var tid = 0
518
+
519
+ if (typeof AbortController !== 'undefined') {
520
+ ctrl = new AbortController()
521
+ tid = setTimeout(function () {
522
+ try { ctrl.abort() } catch (e) {}
523
+ }, 5000)
524
+ }
525
+
526
+ var opts = {
527
+ method: 'GET',
528
+ credentials: 'same-origin',
529
+ cache: 'force-cache',
530
+ headers: { Purpose: 'prefetch' }
531
+ }
532
+ if (ctrl) opts.signal = ctrl.signal
533
+
534
+ try {
535
+ fetch(url, opts)
536
+ .then(function (r) {
537
+ if (tid) clearTimeout(tid)
538
+ if (!r || !r.ok) preloaded.delete(key)
539
+ })
540
+ .catch(function () {
541
+ if (tid) clearTimeout(tid)
542
+ preloaded.delete(key)
543
+ })
544
+ } catch (e) {
545
+ if (tid) clearTimeout(tid)
546
+ preloaded.delete(key)
547
+ }
548
+ }
549
+
550
+ // Viewport Observer
551
+ var vpObserver = null
552
+
553
+ function startViewportObserver() {
554
+ if (vpObserver) return
555
+ vpObserver = new IntersectionObserver(
556
+ function (entries) {
557
+ entries.forEach(function (entry) {
558
+ if (entry.isIntersecting) {
559
+ vpObserver.unobserve(entry.target)
560
+ if (canPreload(entry.target)) preload(entry.target.href, entry.target)
561
+ }
562
+ })
563
+ },
564
+ { rootMargin: isMobile ? '100px' : '200px' }
565
+ )
566
+ observeLinks()
567
+ }
568
+
569
+ function observeLinks() {
570
+ if (!vpObserver) return
571
+ document.querySelectorAll('a').forEach(function (a) {
572
+ if (canPreload(a)) vpObserver.observe(a)
573
+ })
574
+ }
575
+
576
+ // Mutation Observer
577
+ var mutObserver = null
578
+ var mutTimer = null
579
+
580
+ function startMutationObserver() {
581
+ if (mutObserver) return
582
+ mutObserver = new MutationObserver(function (muts) {
583
+ var hasLinks = muts.some(function (m) {
584
+ return Array.from(m.addedNodes).some(function (n) {
585
+ return (
586
+ n.nodeType === 1 &&
587
+ (n.tagName === 'A' || (n.querySelectorAll && n.querySelectorAll('a').length))
588
+ )
589
+ })
590
+ })
591
+ if (hasLinks && vpObserver) {
592
+ clearTimeout(mutTimer)
593
+ mutTimer = setTimeout(observeLinks, 100)
594
+ }
595
+ })
596
+ mutObserver.observe(document.body, { childList: true, subtree: true })
597
+ }
598
+
599
+ // Минимальный публичный API
600
+ var api = {
601
+ version: '1.0.8',
602
+ preload: function (url) { preload(url) }
603
+ }
604
+
605
+ window.Prefetch = api
606
+ return api
607
+ }
608
+
609
+ // Guard от двойной инициализации (если уже есть window.Prefetch — используем его)
610
+ var Prefetch =
611
+ (typeof window !== 'undefined' && window.Prefetch) ? window.Prefetch : createPrefetch(import.meta.url)
612
+
613
+ export { Prefetch }
614
+ export default Prefetch
package/prefetch.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * prefetch.ru v1.0.6 - Мгновенная загрузка страниц
2
+ * prefetch.ru v1.0.8 - Мгновенная загрузка страниц
3
3
  * © 2026 Сергей Макаров | MIT License
4
4
  * https://prefetch.ru | https://github.com/prefetch-ru
5
5
  */
@@ -108,7 +108,12 @@
108
108
  // <body data-prefetch-specrules="prerender">
109
109
  // <body data-prefetch-specrules="no">
110
110
  var hasSr = 'prefetchSpecrules' in ds || 'instantSpecrules' in ds
111
- if (!isIOS && hasSr && HTMLScriptElement.supports && HTMLScriptElement.supports('speculationrules')) {
111
+ if (
112
+ !isIOS &&
113
+ hasSr &&
114
+ HTMLScriptElement.supports &&
115
+ HTMLScriptElement.supports('speculationrules')
116
+ ) {
112
117
  var sr = ds.prefetchSpecrules || ds.instantSpecrules
113
118
  if (sr === 'prerender') {
114
119
  specMode = 'prerender'
@@ -238,7 +243,11 @@
238
243
  var a = getAnchorFromEventTarget(e.target)
239
244
  if (!canPreload(a)) return
240
245
 
241
- a.addEventListener('mouseout', onMouseOut, { passive: true, once: true })
246
+ // v1.0.7: защита от множественных mouseover по вложенным элементам (не плодим таймеры)
247
+ if (hoverTimers.has(a)) return
248
+
249
+ // mouseleave не срабатывает при перемещении внутри ссылки (в отличие от mouseout)
250
+ a.addEventListener('mouseleave', onMouseLeave, { passive: true, once: true })
242
251
 
243
252
  var t = setTimeout(function () {
244
253
  preload(a.href, a)
@@ -247,10 +256,9 @@
247
256
  hoverTimers.set(a, t)
248
257
  }
249
258
 
250
- function onMouseOut(e) {
251
- var a = getAnchorFromEventTarget(e.target)
259
+ function onMouseLeave(e) {
260
+ var a = e.currentTarget
252
261
  if (!a) return
253
- if (e.relatedTarget && e.relatedTarget.closest && a === e.relatedTarget.closest('a')) return
254
262
 
255
263
  var t = hoverTimers.get(a)
256
264
  if (t) {
@@ -268,7 +276,13 @@
268
276
  }
269
277
 
270
278
  function canPreload(a) {
271
- if (!a || !a.href) return false
279
+ if (!a) return false
280
+
281
+ // v1.0.7: исключаем <a href=""> и <a> без href (часто используются как кнопки)
282
+ var hrefAttr = a.getAttribute('href')
283
+ if (hrefAttr === null || hrefAttr.trim() === '') return false
284
+
285
+ if (!a.href) return false
272
286
 
273
287
  // Не навигация в текущей вкладке
274
288
  if (a.target && a.target !== '_self') return false
@@ -296,6 +310,9 @@
296
310
  // Якорь на той же странице
297
311
  if (a.hash && a.pathname + a.search === location.pathname + location.search) return false
298
312
 
313
+ // v1.0.7: не префетчим текущую страницу (в т.ч. для случаев вроде href="")
314
+ if (urlKey(a.href) === urlKey(location.href)) return false
315
+
299
316
  // Уже загружено (ключ НЕ модифицирует реальный URL запроса!)
300
317
  var key = urlKey(a.href)
301
318
  if (preloaded.has(key)) return false
@@ -309,15 +326,31 @@
309
326
  function checkPlatform(a) {
310
327
  var href = a.href
311
328
 
329
+ // v1.0.7: для точных проверок /add /delete /remove используем pathname
330
+ var pathname = ''
331
+ var hash = ''
332
+ try {
333
+ var u = new URL(href, location.href)
334
+ pathname = u.pathname || ''
335
+ hash = u.hash || ''
336
+ } catch (e) {
337
+ pathname = ''
338
+ hash = ''
339
+ }
340
+
312
341
  if (platform === 'bitrix' || platform === 'bitrix24') {
313
342
  if (href.indexOf('/bitrix/') !== -1 || href.indexOf('sessid=') !== -1) return false
314
343
  if (a.classList.contains('bx-ajax')) return false
315
344
  }
316
345
 
317
346
  if (platform === 'tilda') {
318
- if (href.indexOf('#popup:') !== -1 || href.indexOf('#rec') !== -1) return false
347
+ // Можно проверять и по href, но hash надёжнее/дешевле
348
+ if (hash.indexOf('#popup:') !== -1 || hash.indexOf('#rec') !== -1) return false
319
349
  }
320
350
 
351
+ // v1.0.7: /add /delete /remove — только как отдельный сегмент пути (или имя файла типа /delete.php)
352
+ var isActionPath = /(^|\/)(add|delete|remove)(\/|$|\.)/i.test(pathname)
353
+
321
354
  if (
322
355
  href.indexOf('/login') !== -1 ||
323
356
  href.indexOf('/logout') !== -1 ||
@@ -325,12 +358,10 @@
325
358
  href.indexOf('/register') !== -1 ||
326
359
  href.indexOf('/cart') !== -1 ||
327
360
  href.indexOf('/basket') !== -1 ||
328
- href.indexOf('/add') !== -1 ||
329
- href.indexOf('/delete') !== -1 ||
330
- href.indexOf('/remove') !== -1
361
+ isActionPath
331
362
  ) return false
332
363
 
333
- if (/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)/.test(href)) return false
364
+ if (/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)/i.test(href)) return false
334
365
 
335
366
  return true
336
367
  }
@@ -351,7 +382,12 @@
351
382
  if (host === 'googletagmanager.com' || host.endsWith('.googletagmanager.com')) return false
352
383
 
353
384
  if (cls.indexOf('piwik') !== -1 || cls.indexOf('matomo') !== -1) return false
354
- if (host === 'matomo.org' || host.endsWith('.matomo.org') || host === 'piwik.org' || host.endsWith('.piwik.org')) return false
385
+ if (
386
+ host === 'matomo.org' ||
387
+ host.endsWith('.matomo.org') ||
388
+ host === 'piwik.org' ||
389
+ host.endsWith('.piwik.org')
390
+ ) return false
355
391
 
356
392
  return true
357
393
  }
@@ -535,7 +571,7 @@
535
571
 
536
572
  // Минимальный публичный API
537
573
  window.Prefetch = {
538
- version: '1.0.6',
574
+ version: '1.0.8',
539
575
  preload: function (url) { preload(url) }
540
576
  }
541
577
  })()