@prefetchru/prefetch 1.0.7 → 1.0.9

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.9"
102
141
 
103
142
  // Программная предзагрузка URL
104
143
  Prefetch.preload('/catalog/product-123')
@@ -0,0 +1,6 @@
1
+ /*!
2
+ * prefetch.ru v1.0.9 (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.__prefetchRu?window.Prefetch:function(e){if("undefined"==typeof window||"undefined"==typeof document)return{__prefetchRu:!0,version:"1.0.9",preload:function(){}};var t=new Set,n=new WeakMap,r=0,o=0,i=null,a=!1,c=!1,u=null,l=null,s=!1,f=null,d=null,h=!1,p=65,m=80,v=50,g=!1,w=!1,y=!1,x=!1,b="none",L=!1,E=!1,T=!1,A=!1;function S(){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&&N())&&(T=!0);else if(r){var o=parseInt(r,10);!isNaN(o)&&o>=0&&(p=o)}a&&(m=Math.max(60,Math.min(p||0,150))),(A="prefetchObserveDom"in t)||"bitrix"!==l&&"tilda"!==l||(A=!0),"tilda"===l&&p<100&&(p=100);var i={capture:!0,passive:!0};if(document.addEventListener("touchstart",P,i),E?document.addEventListener("mousedown",C,i):document.addEventListener("mouseover",k,i),T)(window.requestIdleCallback||function(e){setTimeout(e,1)})(B,{timeout:1500});A&&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)})})&&q&&(clearTimeout(H),H=setTimeout(U,100))})).observe(document.body,{childList:!0,subtree:!0})}()}}function N(){return!s&&("slow-2g"!==f&&"2g"!==f)}function O(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=Date.now();var t=O(e.target);if(I(t)){o&&(clearTimeout(o),o=0),i&&(document.removeEventListener("touchmove",i,!0),document.removeEventListener("scroll",i,!0),i=null);var n=!1;i=function(){n=!0,o&&(clearTimeout(o),o=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}),o=setTimeout(function(){i&&(document.removeEventListener("touchmove",i,!0),document.removeEventListener("scroll",i,!0),i=null),o=0,n||W(t.href,t)},m)}}function k(e){if(!(r&&Date.now()-r<2500)){var t=O(e.target);if(I(t)&&!n.has(t)){t.addEventListener("mouseleave",M,{passive:!0,once:!0});var o=setTimeout(function(){W(t.href,t),n.delete(t)},p);n.set(t,o)}}}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&&Date.now()-r<2500)){var t=O(e.target);I(t)&&W(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(R(e.href)===R(location.href))return!1;var r=R(e.href);return!t.has(r)&&(!!function(e){var t=e.href,n="",r="";try{var o=new URL(t,location.href);n=o.pathname||"",r=o.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;return!/(^|\/)(login|logout|auth|register|cart|basket|add|delete|remove)(\/|$|\.)/i.test(n)&&!/\.(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 R(e){try{var t=new URL(e,location.href);return t.origin+t.pathname+t.search}catch(t){return e}}function W(e,n){if(N()){var r=function(e){try{var t=new URL(e,location.href);return t.hash="",t.href}catch(t){return e}}(e),o=R(r);if(!t.has(o)){t.size>=v&&t.delete(t.values().next().value),t.add(o);var i=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"!==i)return function(e,t){var n=document.head;if(!n)return;var r=document.createElement("script");r.type="speculationrules",d&&(r.nonce=d);var o={};o[t]=[{source:"list",urls:[e]}],r.textContent=JSON.stringify(o),n.appendChild(r)}(r,i),void(c||!h?D(r,o):_(r,o));c||!h?D(r,o):_(r,o)}}}function _(e,n){var r=document.head;if(r){var o=document.createElement("link");o.rel="prefetch",o.href=e,o.as="document";try{o.fetchPriority="low"}catch(e){}o.onload=i,o.onerror=function(){t.delete(n),i()},r.appendChild(o)}function i(){o.onload=o.onerror=null,o.parentNode&&o.parentNode.removeChild(o)}}function D(e,n){if("function"==typeof fetch){var r=null,o=0;"undefined"!=typeof AbortController&&(r=new AbortController,o=setTimeout(function(){try{r.abort()}catch(e){}},5e3));var i={method:"GET",credentials:"same-origin",cache:"force-cache",headers:{Purpose:"prefetch"}};r&&(i.signal=r.signal);try{fetch(e,i).then(function(e){o&&clearTimeout(o),e&&e.ok||t.delete(n)}).catch(function(){o&&clearTimeout(o),t.delete(n)})}catch(e){o&&clearTimeout(o),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 o=r.nonce||r.getAttribute("nonce")||null;if(o)return o;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 o=/Android/.test(r);(a=(c||o)&&Math.min(screen.width,screen.height)<768)&&(v=20);var i=r.match(/Chrome\/(\d+)/);i&&(u=parseInt(i[1],10));var l=navigator.connection;l&&(f=l.effectiveType,s=l.saveData||!1),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",S):S()}();var q=null;function B(){q||(q=new IntersectionObserver(function(e){e.forEach(function(e){e.isIntersecting&&(q.unobserve(e.target),I(e.target)&&W(e.target.href,e.target))})},{rootMargin:a?"100px":"200px"}),U())}function U(){q&&document.querySelectorAll("a").forEach(function(e){I(e)&&q.observe(e)})}var z=null,H=null,Q={__prefetchRu:!0,version:"1.0.9",preload:function(e){W(e)}};return window.Prefetch=Q,Q}(import.meta.url);export{e as Prefetch};export default e;
@@ -1,6 +1,6 @@
1
1
  /*!
2
- * prefetch.ru v1.0.7 - Мгновенная загрузка страниц
2
+ * prefetch.ru v1.0.9 - Мгновенная загрузка страниц
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,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.7",preload:function(e){W(e)}}}();
6
+ !function(){"use strict";if(!window.Prefetch||!window.Prefetch.__prefetchRu){var e=new Set,t=new WeakMap,n=0,r=0,o=null,i=!1,a=!1,c=null,u=null,s=!1,l=null,f=null,d=!1,h=65,p=80,m=50,v=!1,g=!1,w=!1,y=!1,x="none",L=!1,b=!1,E=!1,T=!1;!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);(i=(a||r)&&Math.min(screen.width,screen.height)<768)&&(m=20);var o=n.match(/Chrome\/(\d+)/);o&&(c=parseInt(o[1],10));var u=navigator.connection;u&&(l=u.effectiveType,s=u.saveData||!1),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",N):N()}();var A=null,S=null,O=null;window.Prefetch={__prefetchRu:!0,version:"1.0.9",preload:function(e){q(e)}}}function N(){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?(x="prerender",y=!0):"no"!==n&&(x="prefetch",y=!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||i&&P())&&(E=!0);else if(r){var o=parseInt(r,10);!isNaN(o)&&o>=0&&(h=o)}i&&(p=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",M,c),b?document.addEventListener("mousedown",W,c):document.addEventListener("mouseover",C,c),E)(window.requestIdleCallback||function(e){setTimeout(e,1)})(B,{timeout:1500});T&&E&&function(){if(S)return;(S=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)})})&&A&&(clearTimeout(O),O=setTimeout(z,100))})).observe(document.body,{childList:!0,subtree:!0})}()}}function P(){return!s&&("slow-2g"!==l&&"2g"!==l)}function k(e){return e?(e.nodeType&&1!==e.nodeType&&(e=e.parentElement),e&&"function"==typeof e.closest?e.closest("a"):null):null}function M(e){n=Date.now();var t=k(e.target);if(D(t)){r&&(clearTimeout(r),r=0),o&&(document.removeEventListener("touchmove",o,!0),document.removeEventListener("scroll",o,!0),o=null);var i=!1;o=function(){i=!0,r&&(clearTimeout(r),r=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}),r=setTimeout(function(){o&&(document.removeEventListener("touchmove",o,!0),document.removeEventListener("scroll",o,!0),o=null),r=0,i||q(t.href,t)},p)}}function C(e){if(!(n&&Date.now()-n<2500)){var r=k(e.target);if(D(r)&&!t.has(r)){r.addEventListener("mouseleave",I,{passive:!0,once:!0});var o=setTimeout(function(){q(r.href,r),t.delete(r)},h);t.set(r,o)}}}function I(e){var n=e.currentTarget;if(n){var r=t.get(n);r&&(clearTimeout(r),t.delete(n))}}function W(e){if(!("number"==typeof e.button&&2===e.button||n&&Date.now()-n<2500)){var t=k(e.target);D(t)&&q(t.href,t)}}function D(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(R(t.href)===R(location.href))return!1;var r=R(t.href);return!e.has(r)&&(!!function(e){var t=e.href,n="",r="";try{var o=new URL(t,location.href);n=o.pathname||"",r=o.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;return!/(^|\/)(login|logout|auth|register|cart|basket|add|delete|remove)(\/|$|\.)/i.test(n)&&!/\.(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 R(e){try{var t=new URL(e,location.href);return t.origin+t.pathname+t.search}catch(t){return e}}function q(t,n){if(P()){var r=function(e){try{var t=new URL(e,location.href);return t.hash="",t.href}catch(t){return e}}(t),o=R(r);if(!e.has(o)){e.size>=m&&e.delete(e.values().next().value),e.add(o);var i=function(e){return y&&"none"!==x?"prerender"!==x?x:L||w||e&&e.dataset&&("prefetchPrerender"in e.dataset||"instantPrerender"in e.dataset)?"prerender":"prefetch":"none"}(n);if("none"!==i)return function(e,t){var n=document.head;if(!n)return;var r=document.createElement("script");r.type="speculationrules",f&&(r.nonce=f);var o={};o[t]=[{source:"list",urls:[e]}],r.textContent=JSON.stringify(o),n.appendChild(r)}(r,i),void(a||!d?U(r,o):_(r,o));a||!d?U(r,o):_(r,o)}}}function _(t,n){var r=document.head;if(r){var o=document.createElement("link");o.rel="prefetch",o.href=t,o.as="document";try{o.fetchPriority="low"}catch(e){}o.onload=i,o.onerror=function(){e.delete(n),i()},r.appendChild(o)}function i(){o.onload=o.onerror=null,o.parentNode&&o.parentNode.removeChild(o)}}function U(t,n){if("function"==typeof fetch){var r=null,o=0;"undefined"!=typeof AbortController&&(r=new AbortController,o=setTimeout(function(){try{r.abort()}catch(e){}},5e3));var i={method:"GET",credentials:"same-origin",cache:"force-cache",headers:{Purpose:"prefetch"}};r&&(i.signal=r.signal);try{fetch(t,i).then(function(t){o&&clearTimeout(o),t&&t.ok||e.delete(n)}).catch(function(){o&&clearTimeout(o),e.delete(n)})}catch(t){o&&clearTimeout(o),e.delete(n)}}}function B(){A||(A=new IntersectionObserver(function(e){e.forEach(function(e){e.isIntersecting&&(A.unobserve(e.target),D(e.target)&&q(e.target.href,e.target))})},{rootMargin:i?"100px":"200px"}),z())}function z(){A&&document.querySelectorAll("a").forEach(function(e){D(e)&&A.observe(e)})}}();
package/package.json CHANGED
@@ -1,15 +1,27 @@
1
1
  {
2
2
  "name": "@prefetchru/prefetch",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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,620 @@
1
+ /*!
2
+ * prefetch.ru v1.0.9 (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 { __prefetchRu: true, version: '1.0.9', 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
+ // v1.0.9: MutationObserver нужен только для viewport режима (отслеживать новые ссылки)
202
+ if (observeDom && viewportMode) 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
+ // v1.0.9: используем Date.now() для единой шкалы времени
227
+ lastTouchTime = Date.now()
228
+
229
+ var a = getAnchorFromEventTarget(e.target)
230
+ if (!canPreload(a)) return
231
+
232
+ // задержка + отмена на scroll/touchmove
233
+ if (touchTimer) {
234
+ clearTimeout(touchTimer)
235
+ touchTimer = 0
236
+ }
237
+ if (touchCancel) {
238
+ document.removeEventListener('touchmove', touchCancel, true)
239
+ document.removeEventListener('scroll', touchCancel, true)
240
+ touchCancel = null
241
+ }
242
+
243
+ var cancelled = false
244
+ touchCancel = function () {
245
+ cancelled = true
246
+ if (touchTimer) {
247
+ clearTimeout(touchTimer)
248
+ touchTimer = 0
249
+ }
250
+ document.removeEventListener('touchmove', touchCancel, true)
251
+ document.removeEventListener('scroll', touchCancel, true)
252
+ touchCancel = null
253
+ }
254
+
255
+ document.addEventListener('touchmove', touchCancel, { capture: true, passive: true, once: true })
256
+ document.addEventListener('scroll', touchCancel, { capture: true, passive: true, once: true })
257
+
258
+ touchTimer = setTimeout(function () {
259
+ if (touchCancel) {
260
+ document.removeEventListener('touchmove', touchCancel, true)
261
+ document.removeEventListener('scroll', touchCancel, true)
262
+ touchCancel = null
263
+ }
264
+ touchTimer = 0
265
+ if (!cancelled) preload(a.href, a)
266
+ }, touchDelay)
267
+ }
268
+
269
+ function onMouseOver(e) {
270
+ // v1.0.9: единая шкала времени Date.now()
271
+ if (lastTouchTime && Date.now() - lastTouchTime < 2500) return
272
+
273
+ var a = getAnchorFromEventTarget(e.target)
274
+ if (!canPreload(a)) return
275
+
276
+ // v1.0.7: защита от множественных mouseover по вложенным элементам (не плодим таймеры)
277
+ if (hoverTimers.has(a)) return
278
+
279
+ // mouseleave не срабатывает при перемещении внутри ссылки (в отличие от mouseout)
280
+ a.addEventListener('mouseleave', onMouseLeave, { passive: true, once: true })
281
+
282
+ var t = setTimeout(function () {
283
+ preload(a.href, a)
284
+ hoverTimers.delete(a)
285
+ }, hoverDelay)
286
+ hoverTimers.set(a, t)
287
+ }
288
+
289
+ function onMouseLeave(e) {
290
+ var a = e.currentTarget
291
+ if (!a) return
292
+
293
+ var t = hoverTimers.get(a)
294
+ if (t) {
295
+ clearTimeout(t)
296
+ hoverTimers.delete(a)
297
+ }
298
+ }
299
+
300
+ function onMouseDown(e) {
301
+ if (typeof e.button === 'number' && e.button === 2) return
302
+ // v1.0.9: единая шкала времени Date.now()
303
+ if (lastTouchTime && Date.now() - lastTouchTime < 2500) return
304
+
305
+ var a = getAnchorFromEventTarget(e.target)
306
+ if (canPreload(a)) preload(a.href, a)
307
+ }
308
+
309
+ function canPreload(a) {
310
+ if (!a) return false
311
+
312
+ // v1.0.7: исключаем <a href=""> и <a> без href (часто используются как кнопки)
313
+ var hrefAttr = a.getAttribute('href')
314
+ if (hrefAttr === null || hrefAttr.trim() === '') return false
315
+
316
+ if (!a.href) return false
317
+
318
+ // Не навигация в текущей вкладке
319
+ if (a.target && a.target !== '_self') return false
320
+ if (a.hasAttribute('download')) return false
321
+
322
+ // Явный запрет
323
+ if ('noPrefetch' in a.dataset || 'prefetchNo' in a.dataset) return false
324
+
325
+ // Белый список
326
+ if (whitelist && !('prefetch' in a.dataset) && !('instant' in a.dataset)) return false
327
+
328
+ // Протокол
329
+ if (a.protocol !== 'http:' && a.protocol !== 'https:') return false
330
+ if (a.protocol === 'http:' && location.protocol === 'https:') return false
331
+
332
+ // Внешние ссылки
333
+ if (a.origin !== location.origin) {
334
+ if (!allowExternal && !('prefetch' in a.dataset) && !('instant' in a.dataset)) return false
335
+ if (!chromiumVer) return false
336
+ }
337
+
338
+ // Query string
339
+ if (a.search && !allowQuery && !('prefetch' in a.dataset) && !('instant' in a.dataset)) return false
340
+
341
+ // Якорь на той же странице
342
+ if (a.hash && a.pathname + a.search === location.pathname + location.search) return false
343
+
344
+ // v1.0.7: не префетчим текущую страницу (в т.ч. для случаев вроде href="")
345
+ if (urlKey(a.href) === urlKey(location.href)) return false
346
+
347
+ // Уже загружено (ключ НЕ модифицирует реальный URL запроса!)
348
+ var key = urlKey(a.href)
349
+ if (preloaded.has(key)) return false
350
+
351
+ if (!checkPlatform(a)) return false
352
+ if (!checkAnalytics(a)) return false
353
+
354
+ return true
355
+ }
356
+
357
+ function checkPlatform(a) {
358
+ var href = a.href
359
+
360
+ // v1.0.7: для точных проверок /add /delete /remove используем pathname
361
+ var pathname = ''
362
+ var hash = ''
363
+ try {
364
+ var u = new URL(href, location.href)
365
+ pathname = u.pathname || ''
366
+ hash = u.hash || ''
367
+ } catch (e) {
368
+ pathname = ''
369
+ hash = ''
370
+ }
371
+
372
+ if (platform === 'bitrix' || platform === 'bitrix24') {
373
+ if (href.indexOf('/bitrix/') !== -1 || href.indexOf('sessid=') !== -1) return false
374
+ if (a.classList.contains('bx-ajax')) return false
375
+ }
376
+
377
+ if (platform === 'tilda') {
378
+ if (hash.indexOf('#popup:') !== -1 || hash.indexOf('#rec') !== -1) return false
379
+ }
380
+
381
+ // v1.0.9: все опасные пути проверяем по сегментам pathname, не по подстроке href
382
+ // Это исправляет ложные блокировки /author, /cartoon, /authentication и т.д.
383
+ var isDangerousPath = /(^|\/)(login|logout|auth|register|cart|basket|add|delete|remove)(\/|$|\.)/i.test(pathname)
384
+
385
+ if (isDangerousPath) return false
386
+
387
+ if (/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)/i.test(href)) return false
388
+
389
+ return true
390
+ }
391
+
392
+ function checkAnalytics(a) {
393
+ var href = a.href
394
+ var cls = a.className || ''
395
+
396
+ // Извлекаем hostname для проверки доменов аналитики
397
+ var host = ''
398
+ try { host = new URL(href, location.href).hostname } catch (e) { host = '' }
399
+
400
+ if (cls.indexOf('ym-') !== -1) return false
401
+ if (host === 'mc.yandex.ru' || host === 'metrika.yandex.ru') return false
402
+
403
+ if (cls.indexOf('ga-') !== -1 || cls.indexOf('gtm-') !== -1) return false
404
+ if (host === 'google-analytics.com' || host.endsWith('.google-analytics.com')) return false
405
+ if (host === 'googletagmanager.com' || host.endsWith('.googletagmanager.com')) return false
406
+
407
+ if (cls.indexOf('piwik') !== -1 || cls.indexOf('matomo') !== -1) return false
408
+ if (
409
+ host === 'matomo.org' ||
410
+ host.endsWith('.matomo.org') ||
411
+ host === 'piwik.org' ||
412
+ host.endsWith('.piwik.org')
413
+ ) return false
414
+
415
+ return true
416
+ }
417
+
418
+ // Ключ для дедупликации: НЕ трогаем pathname (включая / на конце), только убираем hash
419
+ function urlKey(url) {
420
+ try {
421
+ var u = new URL(url, location.href)
422
+ return u.origin + u.pathname + u.search
423
+ } catch (e) {
424
+ return url
425
+ }
426
+ }
427
+
428
+ // URL для реального запроса: абсолютный, без hash, без "улучшений"
429
+ function urlForRequest(url) {
430
+ try {
431
+ var u = new URL(url, location.href)
432
+ u.hash = ''
433
+ return u.href
434
+ } catch (e) {
435
+ return url
436
+ }
437
+ }
438
+
439
+ function resolveSpecMode(a) {
440
+ if (!useSpecRules || specMode === 'none') return 'none'
441
+ if (specMode !== 'prerender') return specMode
442
+
443
+ // specMode === 'prerender'
444
+ if (prerenderAll) return 'prerender'
445
+ if (whitelist) return 'prerender' // ссылки и так явно размечены
446
+
447
+ // иначе prerender только по ссылке:
448
+ if (a && a.dataset && (('prefetchPrerender' in a.dataset) || ('instantPrerender' in a.dataset))) {
449
+ return 'prerender'
450
+ }
451
+ return 'prefetch'
452
+ }
453
+
454
+ function preload(url, a) {
455
+ if (!isNetworkOk()) return
456
+
457
+ var requestUrl = urlForRequest(url)
458
+ var key = urlKey(requestUrl)
459
+
460
+ if (preloaded.has(key)) return
461
+ if (preloaded.size >= maxPreloads) {
462
+ preloaded.delete(preloaded.values().next().value)
463
+ }
464
+ preloaded.add(key)
465
+
466
+ var mode = resolveSpecMode(a)
467
+
468
+ if (mode !== 'none') {
469
+ preloadSpec(requestUrl, mode)
470
+
471
+ // Страховка: обычный prefetch (чтобы под CSP/ограничениями SpecRules всё равно грелся кэш)
472
+ if (isIOS || !supportsLinkPrefetch) preloadFetch(requestUrl, key)
473
+ else preloadLink(requestUrl, key)
474
+ return
475
+ }
476
+
477
+ if (isIOS || !supportsLinkPrefetch) preloadFetch(requestUrl, key)
478
+ else preloadLink(requestUrl, key)
479
+ }
480
+
481
+ function preloadSpec(url, mode) {
482
+ var head = document.head
483
+ if (!head) return
484
+
485
+ var s = document.createElement('script')
486
+ s.type = 'speculationrules'
487
+ if (scriptNonce) s.nonce = scriptNonce
488
+
489
+ var rules = {}
490
+ rules[mode] = [{ source: 'list', urls: [url] }]
491
+ s.textContent = JSON.stringify(rules)
492
+ head.appendChild(s)
493
+ }
494
+
495
+ function preloadLink(url, key) {
496
+ var head = document.head
497
+ if (!head) return
498
+
499
+ var l = document.createElement('link')
500
+ l.rel = 'prefetch'
501
+ l.href = url
502
+ l.as = 'document'
503
+ try { l.fetchPriority = 'low' } catch (e) {}
504
+
505
+ // v1.0.9: удаляем link после загрузки, чтобы не раздувать DOM
506
+ function cleanup() {
507
+ l.onload = l.onerror = null
508
+ if (l.parentNode) l.parentNode.removeChild(l)
509
+ }
510
+
511
+ l.onload = cleanup
512
+ l.onerror = function () { preloaded.delete(key); cleanup() }
513
+ head.appendChild(l)
514
+ }
515
+
516
+ function preloadFetch(url, key) {
517
+ if (typeof fetch !== 'function') return
518
+
519
+ var ctrl = null
520
+ var tid = 0
521
+
522
+ if (typeof AbortController !== 'undefined') {
523
+ ctrl = new AbortController()
524
+ tid = setTimeout(function () {
525
+ try { ctrl.abort() } catch (e) {}
526
+ }, 5000)
527
+ }
528
+
529
+ var opts = {
530
+ method: 'GET',
531
+ credentials: 'same-origin',
532
+ cache: 'force-cache',
533
+ headers: { Purpose: 'prefetch' }
534
+ }
535
+ if (ctrl) opts.signal = ctrl.signal
536
+
537
+ try {
538
+ fetch(url, opts)
539
+ .then(function (r) {
540
+ if (tid) clearTimeout(tid)
541
+ if (!r || !r.ok) preloaded.delete(key)
542
+ })
543
+ .catch(function () {
544
+ if (tid) clearTimeout(tid)
545
+ preloaded.delete(key)
546
+ })
547
+ } catch (e) {
548
+ if (tid) clearTimeout(tid)
549
+ preloaded.delete(key)
550
+ }
551
+ }
552
+
553
+ // Viewport Observer
554
+ var vpObserver = null
555
+
556
+ function startViewportObserver() {
557
+ if (vpObserver) return
558
+ vpObserver = new IntersectionObserver(
559
+ function (entries) {
560
+ entries.forEach(function (entry) {
561
+ if (entry.isIntersecting) {
562
+ vpObserver.unobserve(entry.target)
563
+ if (canPreload(entry.target)) preload(entry.target.href, entry.target)
564
+ }
565
+ })
566
+ },
567
+ { rootMargin: isMobile ? '100px' : '200px' }
568
+ )
569
+ observeLinks()
570
+ }
571
+
572
+ function observeLinks() {
573
+ if (!vpObserver) return
574
+ document.querySelectorAll('a').forEach(function (a) {
575
+ if (canPreload(a)) vpObserver.observe(a)
576
+ })
577
+ }
578
+
579
+ // Mutation Observer
580
+ var mutObserver = null
581
+ var mutTimer = null
582
+
583
+ function startMutationObserver() {
584
+ if (mutObserver) return
585
+ mutObserver = new MutationObserver(function (muts) {
586
+ var hasLinks = muts.some(function (m) {
587
+ return Array.from(m.addedNodes).some(function (n) {
588
+ return (
589
+ n.nodeType === 1 &&
590
+ (n.tagName === 'A' || (n.querySelectorAll && n.querySelectorAll('a').length))
591
+ )
592
+ })
593
+ })
594
+ if (hasLinks && vpObserver) {
595
+ clearTimeout(mutTimer)
596
+ mutTimer = setTimeout(observeLinks, 100)
597
+ }
598
+ })
599
+ mutObserver.observe(document.body, { childList: true, subtree: true })
600
+ }
601
+
602
+ // Минимальный публичный API
603
+ var api = {
604
+ __prefetchRu: true,
605
+ version: '1.0.9',
606
+ preload: function (url) { preload(url) }
607
+ }
608
+
609
+ window.Prefetch = api
610
+ return api
611
+ }
612
+
613
+ // v1.0.9: Guard от двойной инициализации (проверяем маркер __prefetchRu)
614
+ var Prefetch =
615
+ (typeof window !== 'undefined' && window.Prefetch && window.Prefetch.__prefetchRu)
616
+ ? window.Prefetch
617
+ : createPrefetch(import.meta.url)
618
+
619
+ export { Prefetch }
620
+ export default Prefetch
package/prefetch.js CHANGED
@@ -1,11 +1,14 @@
1
1
  /*!
2
- * prefetch.ru v1.0.7 - Мгновенная загрузка страниц
2
+ * prefetch.ru v1.0.9 - Мгновенная загрузка страниц
3
3
  * © 2026 Сергей Макаров | MIT License
4
4
  * https://prefetch.ru | https://github.com/prefetch-ru
5
5
  */
6
6
  ;(function () {
7
7
  'use strict'
8
8
 
9
+ // v1.0.9: guard от двойной инициализации
10
+ if (window.Prefetch && window.Prefetch.__prefetchRu) return
11
+
9
12
  // Состояние
10
13
  var preloaded = new Set()
11
14
  var hoverTimers = new WeakMap()
@@ -170,8 +173,8 @@
170
173
  rIC(startViewportObserver, { timeout: 1500 })
171
174
  }
172
175
 
173
- // Mutation observer
174
- if (observeDom) startMutationObserver()
176
+ // v1.0.9: MutationObserver нужен только для viewport режима (отслеживать новые ссылки)
177
+ if (observeDom && viewportMode) startMutationObserver()
175
178
  }
176
179
 
177
180
  function detectPlatform() {
@@ -195,7 +198,8 @@
195
198
  }
196
199
 
197
200
  function onTouchStart(e) {
198
- lastTouchTime = e.timeStamp || Date.now()
201
+ // v1.0.9: используем Date.now() для единой шкалы времени
202
+ lastTouchTime = Date.now()
199
203
 
200
204
  var a = getAnchorFromEventTarget(e.target)
201
205
  if (!canPreload(a)) return
@@ -238,7 +242,8 @@
238
242
  }
239
243
 
240
244
  function onMouseOver(e) {
241
- if (lastTouchTime && e.timeStamp && e.timeStamp - lastTouchTime < 2500) return
245
+ // v1.0.9: единая шкала времени Date.now()
246
+ if (lastTouchTime && Date.now() - lastTouchTime < 2500) return
242
247
 
243
248
  var a = getAnchorFromEventTarget(e.target)
244
249
  if (!canPreload(a)) return
@@ -269,7 +274,8 @@
269
274
 
270
275
  function onMouseDown(e) {
271
276
  if (typeof e.button === 'number' && e.button === 2) return
272
- if (lastTouchTime && e.timeStamp && e.timeStamp - lastTouchTime < 2500) return
277
+ // v1.0.9: единая шкала времени Date.now()
278
+ if (lastTouchTime && Date.now() - lastTouchTime < 2500) return
273
279
 
274
280
  var a = getAnchorFromEventTarget(e.target)
275
281
  if (canPreload(a)) preload(a.href, a)
@@ -348,18 +354,11 @@
348
354
  if (hash.indexOf('#popup:') !== -1 || hash.indexOf('#rec') !== -1) return false
349
355
  }
350
356
 
351
- // v1.0.7: /add /delete /remove только как отдельный сегмент пути (или имя файла типа /delete.php)
352
- var isActionPath = /(^|\/)(add|delete|remove)(\/|$|\.)/i.test(pathname)
357
+ // v1.0.9: все опасные пути проверяем по сегментам pathname, не по подстроке href
358
+ // Это исправляет ложные блокировки /author, /cartoon, /authentication и т.д.
359
+ var isDangerousPath = /(^|\/)(login|logout|auth|register|cart|basket|add|delete|remove)(\/|$|\.)/i.test(pathname)
353
360
 
354
- if (
355
- href.indexOf('/login') !== -1 ||
356
- href.indexOf('/logout') !== -1 ||
357
- href.indexOf('/auth') !== -1 ||
358
- href.indexOf('/register') !== -1 ||
359
- href.indexOf('/cart') !== -1 ||
360
- href.indexOf('/basket') !== -1 ||
361
- isActionPath
362
- ) return false
361
+ if (isDangerousPath) return false
363
362
 
364
363
  if (/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)/i.test(href)) return false
365
364
 
@@ -479,7 +478,14 @@
479
478
  l.as = 'document'
480
479
  try { l.fetchPriority = 'low' } catch (e) {}
481
480
 
482
- l.onerror = function () { preloaded.delete(key) }
481
+ // v1.0.9: удаляем link после загрузки, чтобы не раздувать DOM
482
+ function cleanup() {
483
+ l.onload = l.onerror = null
484
+ if (l.parentNode) l.parentNode.removeChild(l)
485
+ }
486
+
487
+ l.onload = cleanup
488
+ l.onerror = function () { preloaded.delete(key); cleanup() }
483
489
  head.appendChild(l)
484
490
  }
485
491
 
@@ -571,7 +577,8 @@
571
577
 
572
578
  // Минимальный публичный API
573
579
  window.Prefetch = {
574
- version: '1.0.7',
580
+ __prefetchRu: true,
581
+ version: '1.0.9',
575
582
  preload: function (url) { preload(url) }
576
583
  }
577
584
  })()