Files
2026-hypn0-site/public/static/js/hypn0.js
T

176 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* hypn0.js — Основной клиентский скрипт для hypn0.xyz
* Обеспечивает интеграцию HTMX, работу с Declarative Shadow DOM
* и ленивую асинхронную подгрузку SVG-халфтонов через Alpine.js.
*/
// Автоматическая передача CSRF-токена в AJAX-запросах HTMX
document.addEventListener('htmx:configRequest', function(evt) {
var csrfInput = document.querySelector('[name=csrfmiddlewaretoken]');
var token = csrfInput ? csrfInput.value : null;
if (!token) {
var match = document.cookie.match(/csrftoken=([^;]+)/);
if (match) token = match[1];
}
if (token) {
evt.detail.headers['X-CSRFToken'] = token;
}
});
// Полифилл / обработка Declarative Shadow DOM для старых браузеров и динамических вставок
function attachShadowRoots(root) {
(root || document).querySelectorAll('template[shadowrootmode]').forEach(function(tmpl) {
if (!tmpl.parentElement.shadowRoot) {
var mode = tmpl.getAttribute('shadowrootmode') || 'open';
var shadow = tmpl.parentElement.attachShadow({ mode: mode });
shadow.appendChild(tmpl.content.cloneNode(true));
tmpl.remove();
}
});
}
document.addEventListener('DOMContentLoaded', function() {
attachShadowRoots(document);
});
// Обработка HTMX swap: инициализация теневого корня и Alpine.js компонентов в новом DOM-фрагменте
document.addEventListener('htmx:afterSwap', function(evt) {
attachShadowRoots(evt.detail.target);
if (window.Alpine && evt.detail && evt.detail.target) {
window.Alpine.initTree(evt.detail.target);
}
});
/**
* hypn0LazySvg — Alpine-компонент ленивой асинхронной загрузки SVG в Shadow DOM.
* Защищает от лагов быстрого скролла (AbortController + debounce),
* размазывает пиковые сетевые всплески (джиттер) и изолирует стили/ID.
*/
function hypn0LazySvg(svgUrl) {
return {
svgUrl: svgUrl || '',
loaded: false,
error: false,
_observer: null,
_controller: null,
_timer: null,
init() {
if (!this.svgUrl) return;
var el = this.$el;
if (!('IntersectionObserver' in window)) {
this.fetchSvg(el);
return;
}
var self = this;
this._observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
var jitter = Math.floor(Math.random() * 60);
self._timer = setTimeout(function() {
self.fetchSvg(el);
}, 120 + jitter);
} else {
if (self._timer) {
clearTimeout(self._timer);
self._timer = null;
}
if (self._controller) {
self._controller.abort();
self._controller = null;
}
}
});
}, {
rootMargin: '120px 0px 120px 0px',
threshold: 0.01
});
this._observer.observe(el);
},
fetchSvg(el) {
if (this.loaded) return;
if (this._controller) this._controller.abort();
this._controller = new AbortController();
var self = this;
fetch(this.svgUrl, { signal: this._controller.signal })
.then(function(resp) {
if (!resp.ok) throw new Error('HTTP ' + resp.status);
return resp.text();
})
.then(function(svgText) {
if (!svgText) return;
var shadow = el.shadowRoot;
if (!shadow) {
shadow = el.attachShadow({ mode: 'open' });
}
var style = document.createElement('style');
style.textContent = [
':host {',
' display: flex;',
' width: 100%;',
' height: 100%;',
' align-items: center;',
' justify-content: center;',
' pointer-events: auto;',
' opacity: 0;',
' transition: opacity 0.35s ease-out;',
'}',
':host(.is-loaded), :host([data-loaded]) {',
' opacity: 1;',
'}',
'svg {',
' width: 100%;',
' height: 100%;',
' object-fit: contain;',
'}',
':host(:hover) svg, svg:hover {',
' --hypn0-play: running !important;',
'}'
].join('\n');
var parser = new DOMParser();
var doc = parser.parseFromString(svgText, 'image/svg+xml');
var svgEl = doc.querySelector('svg');
shadow.innerHTML = '';
shadow.appendChild(style);
if (svgEl) {
shadow.appendChild(svgEl);
} else {
var container = document.createElement('div');
container.innerHTML = svgText;
var innerSvg = container.querySelector('svg');
if (innerSvg) shadow.appendChild(innerSvg);
}
self.loaded = true;
self.error = false;
el.setAttribute('data-loaded', 'true');
el.classList.add('is-loaded');
if (self._observer) {
self._observer.disconnect();
self._observer = null;
}
})
.catch(function(err) {
if (err.name === 'AbortError') return;
self.error = true;
});
},
destroy() {
if (this._timer) clearTimeout(this._timer);
if (this._controller) this._controller.abort();
if (this._observer) this._observer.disconnect();
}
};
}