MediaWiki:Common.js
Страница интерфейса MediaWiki
Дополнительные действия
Замечание: Возможно, после публикации вам придётся очистить кэш своего браузера, чтобы увидеть изменения.
- Firefox / Safari: Удерживая клавишу Shift, нажмите на панели инструментов Обновить либо нажмите Ctrl+F5 или Ctrl+R (⌘+R на Mac)
- Google Chrome: Нажмите Ctrl+Shift+R (⌘+Shift+R на Mac)
- Edge: Удерживая Ctrl, нажмите Обновить либо нажмите Ctrl+F5
- Opera: Нажмите Ctrl+F5.
/* ===== PERF (волна 43): адаптивное качество — html.lotm-lite на слабых устройствах =====
Признаки: ≤ 4 потоков CPU, ≤ 4 ГБ памяти, prefers-reduced-motion, «экономия трафика», режим производительности
Citizen, либо замер — средний кадр > 20 мс за 3 с после загрузки (запоминается на 7 дней).
lite: нет нитей фона (canvas), облака тумана спойлеров не дрейфуют (статичная текстура), звёзды/искры Заглавной
без мерцания. Принудительно: ?lotmlite=1 / ?lotmlite=0 (запоминается; ?lotmlite=auto — сброс). Отладка: window.lotmLite. */
(function () {
if (window.lotmLite) { return; }
var H = document.documentElement, K = 'lotm-lite', ls = null, force = null, why = [];
try { ls = window.localStorage; } catch (e) {}
try {
var u = new URLSearchParams(location.search).get('lotmlite');
if (ls && (u === '0' || u === '1')) { ls.setItem(K + '-force', u); }
if (ls && u === 'auto') { ls.removeItem(K + '-force'); ls.removeItem(K + '-slow'); }
force = ls && ls.getItem(K + '-force');
} catch (e) {}
var nav = navigator, mm = window.matchMedia, PM = 'citizen-feature-performance-mode-clientpref-1';
function set(on, r) {
if (on && r && why.indexOf(r) < 0) { why.push(r); }
if (on === H.classList.contains(K)) { return; }
H.classList.toggle(K, on);
try { window.dispatchEvent(new CustomEvent('lotm-lite', { detail: on })); } catch (e) {}
}
window.lotmLite = { on: function () { return H.classList.contains(K); }, why: why, set: set };
if (force === '1') { set(true, 'force'); return; }
if (force === '0') { return; }
if (nav.hardwareConcurrency && nav.hardwareConcurrency <= 2) { why.push('cores'); }
if (nav.deviceMemory && nav.deviceMemory <= 2) { why.push('memory'); }
if (mm && mm('(prefers-reduced-motion: reduce)').matches) { why.push('reduced-motion'); }
if (nav.connection && nav.connection.saveData) { why.push('save-data'); }
if (H.classList.contains(PM)) { why.push('citizen-performance'); }
try { if (ls && +ls.getItem(K + '-slow') > Date.now()) { why.push('slow-frames'); } } catch (e) {}
if (why.length) { set(true); }
/* режим производительности Citizen можно переключить без перезагрузки */
if (window.MutationObserver) {
new MutationObserver(function () {
if (H.classList.contains(PM)) { set(true, 'citizen-performance'); }
else if (why.length === 1 && why[0] === 'citizen-performance') { why.length = 0; set(false); }
}).observe(H, { attributes: true, attributeFilter: ['class'] });
}
if (why.length) { return; }
/* замер: 3 с после загрузки (не во время самой загрузки), только в видимой вкладке */
function measure() {
if (document.hidden || H.classList.contains(K)) { return; }
var nv = navigator; if ((nv.hardwareConcurrency || 0) >= 6 && (!nv.deviceMemory || nv.deviceMemory >= 6)) { return; } /* мощное устройство: не включать lite по кадрам */
var arr = [];
var n = 0, t0 = 0, sum = 0, prev = 0;
function f(t) {
if (document.hidden) { return; }
if (!t0) { t0 = prev = t; } else { sum += t - prev; arr.push(t - prev); n++; prev = t; }
if (t - t0 < 3000) { requestAnimationFrame(f); return; }
arr.sort(function (x, y) { return x - y; }); if (n > 20 && arr[n >> 1] > 34) { /* медиана кадра > 34 мс */
set(true, 'slow-frames');
try { ls && ls.setItem(K + '-slow', String(Date.now() + 864e5)); } catch (e) {}
}
}
requestAnimationFrame(f);
}
if (document.readyState === 'complete') { setTimeout(measure, 600); }
else { window.addEventListener('load', function () { setTimeout(measure, 600); }); }
})();
/* PERF: пауза бесконечных CSS-анимаций Заглавной вне экрана (звёзды, искры) и в скрытой вкладке */
(function () {
if (window.lotmOffscreen) { return; }
window.lotmOffscreen = true;
document.addEventListener('visibilitychange', function () { document.documentElement.classList.toggle('lotm-tab-hidden', document.hidden); });
if (!('IntersectionObserver' in window)) { return; }
var io = new IntersectionObserver(function (l) {
l.forEach(function (e) { e.target.classList.toggle('lotm-offscreen', !e.isIntersecting); });
}, { rootMargin: '100px 0px' });
mw.hook('wikipage.content').add(function ($c) {
var root = ($c && $c[0]) || document, seen = [];
[].forEach.call(root.querySelectorAll('.lotm-stars, .lotm-spark'), function (el) {
var p = el.parentElement;
if (p && seen.indexOf(p) < 0 && !p.dataset.lotmOff) { seen.push(p); p.dataset.lotmOff = '1'; io.observe(p); }
});
});
})();
/* PERF: подписи цитат с названием книги капителью (span style=small-caps) — класс .lotm-q-sc вместо CSS :has([style*=…]),
который заставлял пересчитывать стили всей страницы на каждое изменение inline-стиля */
mw.hook('wikipage.content').add(function ($c) {
var root = ($c && $c[0]) || document;
if (!root.querySelectorAll) { return; }
[].forEach.call(root.querySelectorAll('blockquote.pull-quote .pull-quote__source > div, .blockquote.blockwrapper > div:last-child cite'), function (el) {
if (el.querySelector('[style*="small-caps"]')) { el.classList.add('lotm-q-sc'); }
});
});
/*BackToTopButton*/
window.BackToTopModern = true;
// Custom Tooltip CSS removal
window.tooltips_config = {
offsetX: 20,
offsetY: 20,
waitForImages: true,
noCSS: true,
},
window.tooltips_list = [
{
classname: 'sequence-tooltip',
parse: '{' + '{Template:Seq/data|1=<#seq#>}}'
}, {
classname: 'pathway-tooltip',
parse: '{' + '{Template:Pathways/data|1=<#pathway#>}}'
}, {
classname: 'ingredient-tooltip',
parse: '{' + '{Template:Ingr/data|1=<#name#>}}'
}, {
classname: 'uniqueness-tooltip',
parse: '{' + '{Template:Uniqueness/data|1=<#name#>|2=<#description#>}}'
}
],
/*LockOldComments*/
window.lockOldComments = (window.lockOldComments || {});
window.lockOldComments.limit = 120;
/*Custom Preload*/
window.preloadTemplates_list = "MediaWiki:Custom-PreloadTemplatesList";
window.preloadTemplates_subpage = "case-by-case";
window.preloadTemplates_namespace = "Template";
/* PERF: подсказки нужны только при наведении — грузим в простое после загрузки страницы */
(function () {
function later() {
mw.loader.load('/index.php?title=MediaWiki:Dev-Tooltips.js&action=raw&ctype=text/javascript');
mw.loader.load('/index.php?title=MediaWiki:Dev-ReferencePopups-code.js&action=raw&ctype=text/javascript');
}
function idle() { if (window.requestIdleCallback) { requestIdleCallback(later, { timeout: 2500 }); } else { setTimeout(later, 300); } }
if (document.readyState === 'complete') { idle(); } else { window.addEventListener('load', idle); }
})();
mw.loader.load('/index.php?title=MediaWiki:Dev-Accordion.js&action=raw&ctype=text/javascript');
mw.loader.load('/index.php?title=MediaWiki:CurrencyConversion.js&action=raw&ctype=text/javascript');
/* спойлеры: серый туман v2 — независимые кнопки, облака тумана, доступность */
( function () {
var SH = 'mw-customcollapsible-ShowSpoiler', SHB = 'mw-customcollapsible-ShowSpoilerBox';
var seq = window.lotmSpoilerSeq || 0;
function hasCls( el, c ) { return el.classList && el.classList.contains( c ); }
function isToggler( el ) { return hasCls( el, 'mw-customtoggle-ShowSpoiler' ) || hasCls( el, 'mw-customtoggle-ShowSpoilerBox' ); }
// Выдать группе элементов свой id и перепривязать makeCollapsible (кнопка управляет только своей группой)
function assign( els, base ) {
if ( !els.length ) { return; }
var gid = base + '-' + ( ++seq ), $els = $( els ), made = [];
window.lotmSpoilerSeq = seq;
els.forEach( function ( el ) {
if ( el.id === 'mw-customcollapsible-' + base ) { el.id = 'mw-customcollapsible-' + gid; }
if ( hasCls( el, 'mw-customtoggle-' + base ) ) { el.classList.add( 'mw-customtoggle-' + gid ); el.classList.remove( 'mw-customtoggle-' + base ); }
if ( $( el ).data( 'mw-made-collapsible' ) ) { made.push( el ); }
} );
els.forEach( function ( el ) { el.classList.add( 'lotm-spoiler-own' ); } );
if ( made.length ) {
// makeCollapsible уже успел привязать общий id: снять и привязать заново с новым
$els.off( '.mw-collapsible' );
$( made ).removeData( 'mw-made-collapsible' ).removeClass( 'mw-made-collapsible mw-customtoggle' );
mw.loader.using( 'jquery.makeCollapsible' ).then( function () { $( made ).makeCollapsible(); } );
}
}
// Облака тумана: тело с текстом + слой облаков поверх (3 слоя; на телефоне CSS прячет третий)
var reduceMotion = !!( window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches );
// дрейф облаков — только у блоков в зоне видимости (класс .lotm-fog-live)
var liveIO = ( !reduceMotion && 'IntersectionObserver' in window ) ? new IntersectionObserver( function ( list ) {
var lite = document.documentElement.classList.contains( 'lotm-lite' );
list.forEach( function ( e ) { e.target.classList.toggle( 'lotm-fog-live', e.isIntersecting && !lite ); } );
}, { rootMargin: '120px 0px' } ) : null;
if ( !window.lotmFogVis ) {
window.lotmFogVis = true;
document.addEventListener( 'visibilitychange', function () {
document.documentElement.classList.toggle( 'lotm-fog-paused', document.hidden );
} );
}
function fogify( el ) {
if ( el.classList.contains( 'lotm-fog-ready' ) ) { return; }
var body = document.createElement( 'div' ), sky = document.createElement( 'div' ), i, c;
body.className = 'lotm-fog-body';
while ( el.firstChild ) { body.appendChild( el.firstChild ); }
sky.className = 'lotm-fog-sky';
sky.setAttribute( 'aria-hidden', 'true' );
for ( i = 1; i <= 3; i++ ) {
c = document.createElement( 'i' );
c.className = 'lotm-cloud lotm-cloud-' + i;
sky.appendChild( c );
}
el.appendChild( body );
el.appendChild( sky );
el.classList.add( 'lotm-fog-ready' );
if ( liveIO ) { liveIO.observe( el ); } else if ( !reduceMotion ) { el.classList.add( 'lotm-fog-live' ); }
}
// Скрытый под туманом текст не читается скринридерами и не фокусируется;
// у блоков (animateHeight) высота плавно меняется при раскрытии/сворачивании: под туманом виден только «срез» текста
function a11y( el, animateHeight ) {
var reduce = reduceMotion;
var last = el.classList.contains( 'mw-collapsed' );
var goneTimer = 0, animTimer = 0;
var sync = function () {
var collapsed = el.classList.contains( 'mw-collapsed' );
// раскрытый блок: после разлёта облаков слои тумана выключаются (.lotm-fog-gone), при «Скрыть» — сразу обратно
if ( collapsed !== last ) {
clearTimeout( goneTimer ); clearTimeout( animTimer );
el.classList.add( 'lotm-fog-anim' );
animTimer = setTimeout( function () { el.classList.remove( 'lotm-fog-anim' ); }, 1800 );
if ( collapsed ) { el.classList.remove( 'lotm-fog-gone' ); }
else { goneTimer = setTimeout( function () { if ( !el.classList.contains( 'mw-collapsed' ) ) { el.classList.add( 'lotm-fog-gone' ); } }, 1800 ); }
}
if ( collapsed ) { el.setAttribute( 'aria-hidden', 'true' ); el.setAttribute( 'inert', '' ); }
else { el.removeAttribute( 'aria-hidden' ); el.removeAttribute( 'inert' ); }
if ( animateHeight && !reduce && collapsed !== last && el.firstElementChild && el.firstElementChild.animate ) {
var body = el.firstElementChild, cap = parseFloat( getComputedStyle( body ).fontSize ) * 14,
full = collapsed ? body.scrollHeight : body.offsetHeight,
from = collapsed ? full : Math.min( full, cap ), to = collapsed ? Math.min( full, cap ) : full;
if ( full && from !== to ) {
body.style.overflow = 'clip';
body.animate( [ { height: from + 'px', maxHeight: from + 'px' }, { height: to + 'px', maxHeight: to + 'px' } ], { duration: 1150, easing: 'cubic-bezier(.3,.1,.2,1)' } )
.finished.then( function () { body.style.overflow = ''; }, function () { body.style.overflow = ''; } );
}
}
last = collapsed;
};
sync();
if ( !last ) { el.classList.add( 'lotm-fog-gone' ); }
new MutationObserver( sync ).observe( el, { attributes: true, attributeFilter: [ 'class' ] } );
}
function init( root ) {
var all = [].slice.call( root.querySelectorAll( '[id="' + SH + '"]' ) ), done = [];
var take = function ( list ) { list.forEach( function ( e ) { done.push( e ); } ); return list; };
var free = function ( e ) { return done.indexOf( e ) < 0; };
// 1. Блоки «Показать спойлеры» (Template:SpoilerToggle/context): одна кнопка = один блок
[].forEach.call( root.querySelectorAll( '.spoiler_block' ), function ( blk ) {
var els = all.filter( function ( e ) { return free( e ) && e.closest( '.spoiler_block' ) === blk; } );
if ( !els.length ) { return; }
assign( take( els ), 'ShowSpoiler' );
var content = [].filter.call( blk.children, function ( c ) { return c.classList.contains( 'mw-collapsible' ) && !isToggler( c ); } )[ 0 ];
if ( content ) { fogify( content ); a11y( content, true ); }
} );
// 2. Локальные пары «[Нажмите переключатель]» + содержимое в одном родителе (таблицы, поля инфобокса)
all.filter( function ( e ) { return free( e ) && isToggler( e ); } ).forEach( function ( t ) {
if ( !free( t ) ) { return; }
var sibs = [].filter.call( t.parentElement.children, function ( c ) { return c.id === SH; } );
if ( sibs.some( function ( c ) { return !isToggler( c ); } ) ) { assign( take( sibs ), 'ShowSpoiler' ); }
} );
// 3. Переключатель инфобокса («выкл/вкл»): управляет скрытыми элементами только своего инфобокса
var masters = [], seen = [];
all.filter( function ( e ) { return free( e ) && isToggler( e ); } ).forEach( function ( t ) {
var p = t.parentElement;
if ( seen.indexOf( p ) < 0 ) { seen.push( p ); masters.push( { scope: p.closest( '.portable-infobox' ), els: [] } ); }
masters[ seen.indexOf( p ) ].els.push( t );
} );
all.filter( free ).forEach( function ( e ) {
if ( isToggler( e ) ) { return; }
var box = e.closest( '.portable-infobox' ), m = null;
masters.forEach( function ( x ) { if ( !m && x.scope && x.scope === box ) { m = x; } } );
// вне инфобокса (например, «†» в тексте) подчиняется первому переключателю страницы
if ( !m ) { m = masters[ 0 ]; }
if ( m ) { m.els.push( e ); }
} );
masters.forEach( function ( m ) { assign( m.els, 'ShowSpoiler' ); } );
// 4. Баннеры Template:HideSpoiler: каждый баннер открывает только свой блок
[].forEach.call( root.querySelectorAll( '.SpoilerBox-A' ), function ( a ) {
var b = a.nextElementSibling, t = a.querySelector( '.mw-customtoggle-ShowSpoilerBox' );
if ( !b || !t || b.id !== SHB ) { return; }
assign( [ t, b ], 'ShowSpoilerBox' );
fogify( b );
a11y( b );
} );
}
mw.hook( 'wikipage.content' ).add( function ( $content ) {
$content.each( function () { if ( this.querySelectorAll ) { init( this ); } } );
} );
}() );
/* ===== анимации A1–A3 (v2) ===== */
/* A1: фоновый туман убран по решению владельца (2026-11-01) */
/* A2: наклон карточек Таро за курсором (только устройства с hover) */
mw.hook('wikipage.content').add(function ($c) {
var root = ($c && $c[0]) || document;
var cards = root.querySelectorAll ? root.querySelectorAll('.lotm-tarot') : [];
if (!cards.length || window.matchMedia('(prefers-reduced-motion: reduce)').matches) { return; }
var hover = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
Array.prototype.forEach.call(cards, function (el) {
if (el.getAttribute('data-lotm-tarot')) { return; }
el.setAttribute('data-lotm-tarot', '1');
if (hover) {
var rc = null, mx = 0, my = 0, fr = 0;
var apply = function () {
fr = 0;
if (!rc) { rc = el.getBoundingClientRect(); }
var x = (mx - rc.left) / rc.width - 0.5, y = (my - rc.top) / rc.height - 0.5;
if (!el.classList.contains('lotm-tilting')) { el.classList.add('lotm-tilting'); }
el.style.setProperty('--lotm-ry', (x * 16).toFixed(2) + 'deg');
el.style.setProperty('--lotm-rx', (-y * 16).toFixed(2) + 'deg');
};
el.addEventListener('mouseenter', function () { rc = null; }, { passive: true });
el.addEventListener('mousemove', function (e) {
mx = e.clientX; my = e.clientY;
if (!fr) { fr = requestAnimationFrame(apply); }
}, { passive: true });
el.addEventListener('mouseleave', function () {
if (fr) { cancelAnimationFrame(fr); fr = 0; } rc = null;
el.classList.remove('lotm-tilting');
el.style.removeProperty('--lotm-ry');
el.style.removeProperty('--lotm-rx');
});
} else {
var t;
el.addEventListener('touchstart', function () { clearTimeout(t); el.classList.add('lotm-press'); }, { passive: true });
var end = function () { t = setTimeout(function () { el.classList.remove('lotm-press'); }, 500); };
el.addEventListener('touchend', end, { passive: true });
el.addEventListener('touchcancel', end, { passive: true });
}
});
});
/* ===== спойлеры: spoilerfix ===== */
/* spoilerfix.js — общий переключатель спойлеров + «сироты» (wave30/spoilerfix).
* Не зависит от вида тумана: работает с классами/атрибутами шаблонов
* (.spoilerblur, .spoiler_block, .SpoilerBox-B, [id^=mw-customcollapsible-ShowSpoiler], .spoiler[class*=mw-customtoggle-]).
* Идемпотентен: можно подключать несколько раз / после Common.js. */
( function () {
'use strict';
if ( window.lotmSpoilerFix ) { return; }
window.lotmSpoilerFix = true;
var HINT = 'Нажмите, чтобы открыть спойлер';
var KEY = 'lotm-spoilers-open:' + ( ( window.mw && mw.config && mw.config.get( 'wgPageName' ) ) || location.pathname );
var busy = false;
function store( v ) { try { if ( v === null ) { sessionStorage.removeItem( KEY ); } else { sessionStorage.setItem( KEY, v ? '1' : '0' ); } } catch ( e ) {} }
function stored() { try { return sessionStorage.getItem( KEY ); } catch ( e ) { return null; } }
function has( el, c ) { return !!( el && el.classList && el.classList.contains( c ) ); }
function isToggler( el ) { return !!el.className && /(^|\s)mw-customtoggle(-|\s|$)/.test( el.className ) || /(^|\s)mw-customtoggle-ShowSpoiler/.test( el.className ); }
/* ---------- реестр целей ---------- */
// kind: 'blur' (inline .spoilerblur) | 'block' (mw-collapsible-блок: spoiler_block, SpoilerBox-B, ShowSpoiler-ячейки)
function blocks() {
var out = [], seen = [], ms = masters();
function add( el, kind ) { if ( el && seen.indexOf( el ) < 0 ) { seen.push( el ); out.push( { el: el, kind: kind } ); } }
[].forEach.call( document.querySelectorAll( '.spoilerblur' ), function ( e ) { add( e, 'blur' ); } );
[].forEach.call( document.querySelectorAll( '.spoiler_block > .mw-collapsible, .SpoilerBox-B, [id^="mw-customcollapsible-ShowSpoiler"]' ), function ( e ) {
if ( ms.indexOf( e ) >= 0 || has( e, 'spoiler_block-headerT' ) || /(^|\s)mw-customtoggle-/.test( e.className ) && !/mw-collapsible/.test( e.className ) ) { return; }
if ( e.closest( '.spoiler_block-header' ) ) { return; }
add( e, 'block' );
} );
return out;
}
function isClosed( t ) { return t.kind === 'blur' ? !has( t.el, 'lotm-revealed' ) : has( t.el, 'mw-collapsed' ); }
// «своя» кнопка блока: видимый переключатель, который управляет этим блоком
function ownToggle( el ) {
var m = ( el.id || '' ).match( /^mw-customcollapsible-(.+)$/ ), list, vis;
if ( !m ) { return null; }
list = [].slice.call( document.querySelectorAll( '.mw-customtoggle-' + m[ 1 ] ) );
var ms = masters();
list = list.filter( function ( x ) { return ms.indexOf( x ) < 0 && x !== el && !el.contains( x ) && !has( x, 'basic-tooltip' ); } );
vis = list.filter( function ( x ) { return !has( x, 'mw-collapsed' ); } )[ 0 ];
return list.length > 2 ? null : ( vis || list[ 0 ] || null );
}
function setBlock( t, open ) {
if ( open === !isClosed( t ) ) { return; }
if ( t.kind === 'blur' ) {
t.el.classList.toggle( 'lotm-revealed', open );
t.el.setAttribute( 'aria-expanded', open ? 'true' : 'false' );
t.el.setAttribute( 'aria-pressed', open ? 'true' : 'false' );
return;
}
var tg = ownToggle( t.el );
if ( tg ) { tg.click(); }
if ( open === has( t.el, 'mw-collapsed' ) ) { // кнопки нет / не сработала: переключаем напрямую
t.el.classList.toggle( 'mw-collapsed', !open );
t.el.classList.toggle( 'mw-made-collapsible', true );
}
}
/* ---------- общий переключатель ---------- */
function masters() {
var by = [], out = [];
[].forEach.call( document.querySelectorAll( '.spoiler[class*="mw-customtoggle-"]' ), function ( e ) {
if ( has( e, 'spoiler_block-headerT' ) || has( e, 'basic-tooltip' ) || !e.parentElement ) { return; }
var i = by.indexOf( e.parentElement ); if ( i < 0 ) { by.push( e.parentElement ); out.push( [] ); i = by.length - 1; }
out[ i ].push( e );
} );
return out.filter( function ( g ) { return g.length === 2; } ).reduce( function ( a, g ) { return a.concat( g ); }, [] );
}
// пара «выкл»/«вкл»: первая в DOM = «выкл» (нажатие открывает спойлеры)
function openFromMaster( el ) {
var sibs = [].filter.call( el.parentElement ? el.parentElement.children : [], function ( c ) { return masters().indexOf( c ) >= 0; } );
return sibs.length > 1 ? sibs.indexOf( el ) === 0 : true;
}
function syncMasters( open ) {
var ms = masters(), seen = [];
ms.forEach( function ( m ) {
var p = m.parentElement; if ( seen.indexOf( p ) >= 0 ) { return; } seen.push( p );
var pair = [].filter.call( p.children, function ( c ) { return ms.indexOf( c ) >= 0; } );
if ( pair.length < 2 ) { return; }
var off = pair[ 0 ], on = pair[ 1 ];
// open: виден «вкл» (кнопка «выкл» свёрнута); closed: наоборот
if ( open && !has( off, 'mw-collapsed' ) ) { busy = true; try { off.click(); } finally { busy = false; } }
else if ( !open && has( off, 'mw-collapsed' ) ) { busy = true; try { on.click(); } finally { busy = false; } }
} );
}
function setAll( open, fromMaster ) {
store( open );
if ( !fromMaster ) { syncMasters( open ); } // сначала общий переключатель (он нативно листает блоки с общим id), потом добиваем остальное
blocks().forEach( function ( t ) { setBlock( t, open ); } );
refresh();
}
function bindMasters() {
masters().forEach( function ( m ) {
if ( m.dataset.lotmSfMaster ) { return; }
m.dataset.lotmSfMaster = '1';
var h = function ( e ) {
if ( busy ) { return; }
if ( e.type === 'keydown' && e.which !== 13 && e.which !== 32 ) { return; }
var open = openFromMaster( m );
setTimeout( function () { setAll( open, true ); }, 0 );
setTimeout( function () { setAll( open, true ); }, 60 ); // страховка от гонок с другими обработчиками
};
m.addEventListener( 'click', h );
m.addEventListener( 'keydown', h );
} );
}
/* ---------- inline .spoilerblur: тап / Enter / пробел ---------- */
function bindBlur() {
[].forEach.call( document.querySelectorAll( '.spoilerblur' ), function ( sb ) {
sb.setAttribute( 'tabindex', '0' );
sb.setAttribute( 'role', 'button' );
if ( !sb.hasAttribute( 'aria-expanded' ) ) { sb.setAttribute( 'aria-expanded', has( sb, 'lotm-revealed' ) ? 'true' : 'false' ); }
if ( !has( sb, 'lotm-revealed' ) ) { sb.setAttribute( 'aria-label', HINT ); sb.title = HINT; } else { sb.removeAttribute( 'aria-label' ); sb.removeAttribute( 'title' ); }
if ( sb.dataset.lotmBlur || sb.dataset.lotmSf ) { return; }
sb.dataset.lotmSf = '1'; sb.dataset.lotmBlur = '1'; // второй обработчик Common.js не навешиваем дважды
var flip = function ( e ) {
if ( e.type === 'keydown' && e.which !== 13 && e.which !== 32 ) { return; }
if ( e.type === 'click' && has( sb, 'lotm-revealed' ) && e.target.closest && e.target.closest( 'a' ) ) { return; }
if ( e.type === 'click' && String( window.getSelection() ) && has( sb, 'lotm-revealed' ) ) { return; } // выделение открытого текста не сворачивает
e.preventDefault();
setBlock( { el: sb, kind: 'blur' }, !has( sb, 'lotm-revealed' ) );
refresh();
};
sb.addEventListener( 'click', flip );
sb.addEventListener( 'keydown', flip );
} );
}
/* ---------- блоки: тап по самому блоку (устойчиво к inert/pointer-events у тумана) ---------- */
function onDocClick( e ) {
if ( e.defaultPrevented || e.button > 0 ) { return; }
var x = e.clientX, y = e.clientY, hit = null;
if ( e.target.closest && e.target.closest( 'a[href], button, input, textarea, select, [class*="mw-customtoggle-"]' ) ) { return; }
blocks().forEach( function ( t ) {
if ( hit || t.kind !== 'block' || !isClosed( t ) ) { return; }
var r = t.el.getBoundingClientRect();
if ( r.width && r.height && x >= r.left && x <= r.right && y >= r.top && y <= r.bottom ) { hit = t; }
} );
if ( hit ) { e.preventDefault(); e.stopPropagation(); setBlock( hit, true ); refresh(); }
}
// клавиатура: фокус на самом блоке, если он не inert
function bindBlockA11y() {
cached = null; blocks().forEach( function ( t ) {
var el = t.el, closed = isClosed( t ), tg;
if ( t.kind === 'blur' ) { return; }
tg = ownToggle( el );
if ( tg ) { tg.setAttribute( 'aria-expanded', closed ? 'false' : 'true' ); if ( !tg.hasAttribute( 'tabindex' ) ) { tg.setAttribute( 'tabindex', '0' ); } if ( !tg.hasAttribute( 'role' ) ) { tg.setAttribute( 'role', 'button' ); } }
if ( el.dataset.lotmSfKey ) { return; }
el.dataset.lotmSfKey = '1';
el.addEventListener( 'keydown', function ( e ) {
if ( e.target !== el || ( e.which !== 13 && e.which !== 32 ) || !has( el, 'mw-collapsed' ) ) { return; }
e.preventDefault(); setBlock( t, true ); refresh();
} );
} );
var bl = blocks(), cur = bl.map( function ( t ) { return t.el; } );
[].forEach.call( document.querySelectorAll( '.lotm-sf-closed' ), function ( el ) { if ( cur.indexOf( el ) < 0 ) { el.classList.remove( 'lotm-sf-closed' ); } } );
bl.forEach( function ( t ) {
if ( t.kind !== 'block' ) { return; }
var closed = isClosed( t );
if ( closed !== has( t.el, 'lotm-sf-closed' ) ) { t.el.classList.toggle( 'lotm-sf-closed', closed ); }
if ( closed && !t.el.hasAttribute( 'inert' ) ) { t.el.setAttribute( 'tabindex', '0' ); t.el.setAttribute( 'role', 'button' ); t.el.setAttribute( 'aria-label', HINT ); t.el.setAttribute( 'aria-expanded', 'false' ); }
else if ( !closed && t.el.dataset.lotmSfKey ) { t.el.removeAttribute( 'tabindex' ); if ( t.el.getAttribute( 'role' ) === 'button' ) { t.el.removeAttribute( 'role' ); } t.el.removeAttribute( 'aria-label' ); t.el.removeAttribute( 'aria-expanded' ); }
} );
}
/* ---------- подсказка «Нажмите, чтобы открыть спойлер» поверх закрытых целей ---------- */
var layer, pills = [], raf = 0, cached = null;
function ensureLayer() {
if ( layer && layer.isConnected ) { return; }
layer = document.createElement( 'div' );
layer.className = 'lotm-sf-layer'; layer.setAttribute( 'aria-hidden', 'true' );
document.body.appendChild( layer );
}
function place() {
raf = 0; ensureLayer();
if ( !cached ) { cached = blocks().filter( isClosed ); }
var ts = cached, vh = window.innerHeight, vw = document.documentElement.clientWidth, pos = [];
while ( pills.length < ts.length ) { var p = document.createElement( 'span' ); p.className = 'lotm-sf-pill'; p.textContent = HINT; layer.appendChild( p ); pills.push( p ); }
// 1) только чтения
pills.forEach( function ( p, i ) {
var t = ts[ i ], r, top, bottom;
if ( !t || !t.el.isConnected ) { pos[ i ] = null; return; }
r = t.el.getBoundingClientRect(); // для многострочного inline — общий прямоугольник
top = Math.max( r.top, 0 ); bottom = Math.min( r.bottom, vh );
if ( !r.width || bottom - top < 14 || r.right < 0 || r.left > vw ) { pos[ i ] = null; return; }
pos[ i ] = [ Math.max( 0, r.left ) + ( Math.min( r.right, vw ) - Math.max( 0, r.left ) ) / 2, top + ( bottom - top ) / 2 ];
} );
// 2) только записи (и только изменившиеся значения)
pills.forEach( function ( p, i ) {
var q = pos[ i ], s = p.style;
if ( !q ) { if ( s.display !== 'none' ) { s.display = 'none'; } return; }
var l = q[ 0 ] + 'px', tp = q[ 1 ] + 'px';
if ( s.display ) { s.display = ''; }
if ( s.left !== l ) { s.left = l; }
if ( s.top !== tp ) { s.top = tp; }
} );
}
function refresh() { bindBlur(); bindBlockA11y(); cached = null; if ( !raf ) { raf = requestAnimationFrame( place ); } }
// прокрутка/ресайз: только перестановка подсказок (без пересборки реестра и перепривязки), не чаще кадра
function reposition() { if ( !raf ) { raf = requestAnimationFrame( place ); } }
/* ---------- запуск ---------- */
function init() {
bindMasters(); bindBlur();
var s = stored();
if ( s === '1' ) { setAll( true, false ); } else { refresh(); }
}
if ( !document.body.dataset.lotmSfDoc ) {
document.body.dataset.lotmSfDoc = '1';
document.addEventListener( 'click', onDocClick, true );
window.addEventListener( 'scroll', reposition, { passive: true } );
window.addEventListener( 'resize', reposition, { passive: true } );
window.addEventListener( 'hashchange', function () { var s = stored(); if ( s === '1' ) { setAll( true, false ); } } );
window.addEventListener( 'pageshow', function () { setTimeout( init, 50 ); } );
var OWN = /^(lotm-(sf-closed|fog-live|fog-anim|fog-gone|tilting|press|offscreen))$/, mo = 0;
var significant = function ( r ) {
var t = r.target;
if ( t === layer || ( t.classList && t.classList.contains( 'lotm-sf-pill' ) ) ) { return false; }
var a = ( r.oldValue || '' ).split( /\s+/ ), b = ( t.getAttribute( 'class' ) || '' ).split( /\s+/ ), i;
for ( i = 0; i < a.length; i++ ) { if ( a[ i ] && b.indexOf( a[ i ] ) < 0 && !OWN.test( a[ i ] ) ) { return true; } }
for ( i = 0; i < b.length; i++ ) { if ( b[ i ] && a.indexOf( b[ i ] ) < 0 && !OWN.test( b[ i ] ) ) { return true; } }
return false;
};
new MutationObserver( function ( recs ) {
if ( mo || !recs.some( significant ) ) { return; }
mo = requestAnimationFrame( function () { mo = 0; bindMasters(); refresh(); } );
} ).observe( document.body, { attributes: true, attributeFilter: [ 'class' ], attributeOldValue: true, subtree: true } );
}
if ( window.mw && mw.hook ) { mw.hook( 'wikipage.content' ).add( function () { setTimeout( init, 400 ); } ); }
else { setTimeout( init, 400 ); }
}() );
/* ===== примечания ===== */
/* refs.js — раздел «Примечания»: скрыть пустые, убрать дубли заголовков, почистить оглавление */
(function(){
function run(){
var secs=[].slice.call(document.querySelectorAll('.mw-parser-output section.citizen-section, #mw-content-text section.citizen-section'));
var seen=false;
secs.forEach(function(s){
var h=s.querySelector(':scope > h2, :scope > .citizen-section-heading');
if(!h)return;
var t=h.textContent.replace(/\s+/g,' ').trim();
if(!/^Примечания$/i.test(t))return;
var body=s.querySelector(':scope > .citizen-section-body')||s;
var has=!!s.querySelector('ol.references')||body.textContent.replace(/\s+/g,'').length>0&&!body.querySelector('.lotm-refs')||!!body.querySelector('img,table,ul,ol,pre');
var drop=!has||seen;
if(has&&!seen)seen=true;
if(drop){
s.classList.add('lotm-refs-hidden');
var id=(h.querySelector('.mw-headline')||{}).id;
if(id){
[].slice.call(document.querySelectorAll('a[href="#'+id+'"], a[href="#'+encodeURIComponent(id)+'"]')).forEach(function(a){
var li=a.closest('li'); if(li&&!li.querySelector('ul li a:not([href="#'+id+'"])'))li.classList.add('lotm-refs-hidden');
});
}
}
});
}
function go(){ if(window.requestIdleCallback)requestIdleCallback(run,{timeout:1500});else setTimeout(run,0); }
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',go);else go();
})();
/* ===== BGT v3 — нити марионеток (духовное тело Марионеточника, Путь Шута), быстрая отрисовка =====
Вид — как v2: в каждом поле вокруг панели статьи 1–2 невидимые «руки кукловода» высоко над экраном, от них
веером провисающие серебристые нити с грузиком/крючком, пылинки, изредка искра по нити. Всё — функции реального
времени (общий старт и зерно — в sessionStorage): после перехода на другую статью всё продолжается с той же фазы.
v3 (скорость): вместо canvas на весь экран — два узких canvas только на поля (DPR ≤ 1.5, contain:strict,
свой композитный слой); поле уже 40 px не рисуется; градиенты нитей и спрайт свечения — заранее, без shadowBlur.
Рисует Worker через OffscreenCanvas (transferControlToOffscreen): прокрутка и тяжёлый главный поток нити
не стопорят — ровные 30 кадров/с и при листании (после 15 с без действий — 15). Нет Worker/OffscreenCanvas
или blob: запрещён — то же ядро в главном потоке. Скрытая вкладка — пауза; lite-режим — нитей нет;
prefers-reduced-motion — один статичный кадр. Нет на телефоне (< 721px) и в светлой теме (CSS). */
(function () {
if (window.lotmBgt) { return; }
var mqDesk = window.matchMedia('(min-width: 721px)'), mqRM = window.matchMedia('(prefers-reduced-motion: reduce)');
var START = Date.now(), SEED = (Math.random() * 4294967296) >>> 0, FRESH = true;
try {
var st = JSON.parse(sessionStorage.getItem('lotmBgt') || 'null');
if (st && st.s > 0 && st.s <= START && START - st.s < 864e5) { START = st.s; SEED = st.k >>> 0; FRESH = false; }
else { sessionStorage.setItem('lotmBgt', JSON.stringify({ s: START, k: SEED })); }
} catch (e) {}
/* Ядро: не обращается ни к чему снаружи (исходник уходит в Worker через toString). G: seed, start, cvs[2], raf, caf, stat */
function core(G) {
var SEED = G.seed, START = G.start, N = 34, DPR = 1, W = 0, H = 0, sides = [], threads = [], motes = [], glow;
var T0 = performance.timeOrigin || (Date.now() - performance.now());
var on = false, hid = false, raf = 0, prevT = 0, prevTs = 0, lastDraw = 0, ema = 16, lastInput = performance.now(), FRAME = 1000 / 30 - 2;
var still = false, stA = [], stN = 0, stT = 0, mouse = { x: -1e4, y: -1e4, in: false };
var SILVER = [208, 224, 255], HALO_S = [130, 170, 255];
function now() { return (T0 + performance.now() - START) / 1000; }
function rand(a, b) { /* mulberry32 от (зерно, a, b) */
var t = (SEED ^ Math.imul(a + 1, 0x9E3779B1) ^ Math.imul(b + 7, 0x85EBCA77)) >>> 0;
return function () { t = (t + 0x6D2B79F5) >>> 0; var r = Math.imul(t ^ (t >>> 15), 1 | t); r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r; return ((r ^ (r >>> 14)) >>> 0) / 4294967296; };
}
function rgba(c, a) { return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + Math.max(0, a).toFixed(3) + ')'; }
function sprite() {
var s = 64, c = typeof OffscreenCanvas !== 'undefined' ? new OffscreenCanvas(s, s) : document.createElement('canvas'), x, g;
c.width = c.height = s; x = c.getContext('2d'); g = x.createRadialGradient(s / 2, s / 2, 0, s / 2, s / 2, s / 2);
g.addColorStop(0, 'rgba(255,255,255,1)'); g.addColorStop(0.12, 'rgba(225,238,255,.85)');
g.addColorStop(0.35, 'rgba(160,195,255,.22)'); g.addColorStop(1, 'rgba(120,160,255,0)');
x.fillStyle = g; x.fillRect(0, 0, s, s);
return c;
}
glow = sprite();
function build() {
threads = []; motes = [];
sides.forEach(function (sd, si) {
if (!sd.on) { sd.n = 0; return; }
var narrow = sd.w < 160;
sd.n = narrow ? (sd.w < 60 ? 2 : 3) : Math.max(6, Math.min(9, Math.round(sd.w / 50)));
var hr = rand(si * 13 + 5, 0), hands = [], nh = narrow ? 1 : 2, h, i;
for (h = 0; h < nh; h++) {
hands.push({ x: sd.x + sd.w * (nh === 1 ? 0.5 : (h ? 0.7 : 0.3)) + (hr() - 0.5) * sd.w * 0.12, y: -H * (0.13 + 0.1 * hr()) - 40, p: hr() * 6.283 });
}
for (i = 0; i < sd.n; i++) {
var r = rand(si * 31 + 3, i), hand = hands[nh === 1 || i < sd.n / 2 ? 0 : 1];
var z = narrow ? 0.7 + 0.3 * r() : [0.95, 0.35, 0.7, 0.2, 0.85, 0.5, 0.3, 0.75, 0.6][i % 9] * (0.85 + 0.3 * r());
z = Math.min(1, z);
var ex = sd.x + (narrow ? sd.w * (0.25 + 0.5 * (sd.n > 1 ? i / (sd.n - 1) : 0.5)) : 10 + (sd.w - 20) * ((i + 0.5 + (r() - 0.5) * 0.5) / sd.n));
var ey = H * (r() < 0.4 ? 0.38 + 0.2 * r() : 0.62 + 0.4 * r());
var len = Math.hypot(ex - hand.x, ey - hand.y);
var w0 = 2 * Math.PI / (2 * Math.sqrt(len / 300) + 3) * (1.15 - 0.3 * (1 - z));
threads.push({
side: si, hand: hand, ex: ex, ey: ey, len: len, z: z,
lw: 0.6 + 1.0 * z, a: (narrow ? 0.26 : 0.12) + 0.42 * z * z,
w1: w0, w2: w0 * (0.37 + 0.1 * r()), w3: w0 * (2.3 + 0.6 * r()),
p1: r() * 6.283, p2: r() * 6.283, p3: r() * 6.283,
A1: len * (0.012 + 0.012 * r()),
S0: (r() < 0.5 ? -1 : 1) * (0.025 + 0.035 * r()) * len, S1: (0.015 + 0.02 * r()) * len,
A3: 2 + 3 * r(),
end: r() < 0.45 ? 1 : r() < 0.6 ? 2 : 0,
hookDir: r() < 0.5 ? -1 : 1,
px: new Float32Array(N + 1), py: new Float32Array(N + 1),
pull: 0, cs: 0.7, tx: 0, ty: 0, d: 1e9, near: false
});
}
var nm = Math.round(sd.w / 45);
for (var m = 0; m < nm; m++) {
var q = rand(si * 97 + 11, m);
motes.push({ side: si, fx: q(), y0: q(), v: 4 + 7 * q(), ax: 6 + 10 * q(), wx: 0.05 + 0.08 * q(), ph: q() * 6.283, tw: 0.3 + 0.5 * q(), s: 1.2 + 1.4 * q(), a: 0.05 + 0.1 * q() });
}
});
/* градиенты по длине нити (абсолютные y): выход из темноты сверху, таяние к низу — один раз на геометрию */
threads.forEach(function (th) {
var x = sides[th.side].cx, c = SILVER, hc = HALO_S, a = th.a, y1 = th.ey;
var f = function (y) { return Math.max(0, Math.min(1, y / y1)); };
var g1 = x.createLinearGradient(0, 0, 0, y1);
g1.addColorStop(0, rgba(c, 0)); g1.addColorStop(f(130), rgba(c, a * 0.8)); g1.addColorStop(f(Math.min(y1 * 0.45, 380)), rgba(c, a));
g1.addColorStop(0.8, rgba(c, a * 0.45)); g1.addColorStop(1, rgba(c, a * 0.1));
var g0 = x.createLinearGradient(0, 0, 0, y1);
g0.addColorStop(0, rgba(hc, 0)); g0.addColorStop(f(170), rgba(hc, a * 0.2)); g0.addColorStop(0.55, rgba(hc, a * 0.22));
g0.addColorStop(1, rgba(hc, 0));
th.g0 = g0; th.g1 = g1; th.col = rgba(c, 1); th.colE = rgba(c, th.a * 0.55);
});
}
function setGeom(g) {
W = g.W; H = g.H; DPR = g.DPR;
sides = g.s.map(function (s, i) {
var sd = { x: s[0], w: s[1], on: s[1] >= 40 }, c = G.cvs[i], x;
c.width = sd.on ? Math.max(1, Math.round(sd.w * DPR)) : 1; c.height = sd.on ? Math.max(1, Math.round(H * DPR)) : 1;
x = c.getContext('2d'); x.setTransform(DPR, 0, 0, DPR, -sd.x * DPR, 0); x.lineCap = 'round'; x.lineJoin = 'round';
sd.cx = x; return sd;
});
build();
}
function posThread(th, t, dt) {
var hd = th.hand, hx = hd.x + 8 * Math.sin(0.041 * t + hd.p) + 3 * Math.sin(0.13 * t + hd.p * 2), hy = hd.y + 4 * Math.sin(0.07 * t + hd.p);
var sw = Math.sin(th.w1 * t + th.p1), ex = th.ex + th.A1 * sw + 6 * Math.sin(0.23 * t - th.ex * 0.004), ey = th.ey - Math.abs(th.A1 * sw) * th.A1 / th.len * 0.5;
var dx = ex - hx, dy = ey - hy, L = Math.hypot(dx, dy), nx = -dy / L, ny = dx / L;
var sag = th.S0 + th.S1 * Math.sin(th.w2 * t + th.p2) - th.A1 * sw * 0.35;
var s0 = Math.max(0, Math.min(0.95, (-14 - hy) / dy)), i, s, b, o;
for (i = 0; i <= N; i++) {
s = s0 + (1 - s0) * i / N;
b = 4 * s * (1 - s);
o = sag * b + th.A3 * Math.sin(9 * s - th.w3 * t + th.p3) * b * s;
th.px[i] = hx + dx * s + nx * o; th.py[i] = hy + dy * s + ny * o;
}
var tgt = 0; th.d = 1e9;
if (mouse.in && dt > 0) {
var best = 1e9, bi = 0, ddx, ddy, d;
for (i = 4; i <= N; i += 2) { ddx = th.px[i] - mouse.x; ddy = th.py[i] - mouse.y; d = ddx * ddx + ddy * ddy; if (d < best) { best = d; bi = i; } }
th.d = best;
if (th.near && best < 130 * 130) { tgt = 1 - best / (130 * 130); th.cs += (bi / N - th.cs) * Math.min(1, dt * 3); th.tx = mouse.x; th.ty = mouse.y; }
}
if (dt > 0) { th.pull += (tgt - th.pull) * (1 - Math.exp(-dt / (tgt > th.pull ? 0.45 : 1.1))); }
if (th.pull > 0.002) {
var ci = Math.max(1, Math.min(N, Math.round(th.cs * N))), ox = th.tx - th.px[ci], oy = th.ty - th.py[ci], m = Math.sqrt(ox * ox + oy * oy), cap = 70;
if (m > cap) { ox *= cap / m; oy *= cap / m; }
var k = th.pull * 0.55 * (0.6 + 0.4 * th.z);
for (i = 1; i <= N; i++) {
s = i / N; var u = (s - th.cs) / (s < th.cs ? Math.max(0.15, th.cs * 0.9) : 0.22), w = Math.exp(-u * u) * Math.pow(s, 0.7);
th.px[i] += ox * k * w; th.py[i] += oy * k * w * 0.7;
}
}
}
function path(x, th) {
var px = th.px, py = th.py, i;
x.moveTo(px[0], py[0]);
for (i = 1; i < N; i++) { x.quadraticCurveTo(px[i], py[i], (px[i] + px[i + 1]) / 2, (py[i] + py[i + 1]) / 2); }
x.lineTo(px[N], py[N]);
}
var AT = [0, 0];
function at(th, s) {
var f = Math.max(0, Math.min(N, s * N)), i = Math.min(N - 1, Math.floor(f)), u = f - i;
AT[0] = th.px[i] + (th.px[i + 1] - th.px[i]) * u; AT[1] = th.py[i] + (th.py[i + 1] - th.py[i]) * u; return AT;
}
function dot(x, px, py, r, a) { x.globalAlpha = a; x.drawImage(glow, px - r, py - r, r * 2, r * 2); }
var SP = 7;
function sparks(t) {
for (var n = Math.floor(t / SP) - 1; n <= Math.floor(t / SP); n++) {
var r = rand(9001, n & 0x7fffffff);
if (r() < 0.35 || !threads.length) { continue; }
var th = threads[Math.floor(r() * threads.length)], dur = 3 + 1.5 * r(), st = n * SP + r() * 2.5, u = (t - st) / dur;
if (u < 0 || u > 1) { continue; }
var x = sides[th.side].cx, e = u * u * (3 - 2 * u), s = 0.05 + 0.85 * e, fade = Math.min(1, u * 6, (1 - u) * 3) * (1 - 0.6 * e), A = fade * (0.5 + 0.5 * th.z), p;
for (var k = 7; k >= 1; k--) { p = at(th, s - k * 0.012); dot(x, p[0], p[1], 3 + 2 * th.z, A * 0.2 * (1 - k / 8)); }
p = at(th, s);
dot(x, p[0], p[1], 13 + 6 * th.z, A * 0.3); dot(x, p[0], p[1], 3.5 + 1.5 * th.z, A * 0.9);
}
}
function byD(p, q) { return p.d - q.d; }
function draw(t, dt) {
var i, th, x, sd;
for (i = 0; i < sides.length; i++) {
sd = sides[i]; if (!sd.on) { continue; }
x = sd.cx; x.globalCompositeOperation = 'source-over'; x.globalAlpha = 1; x.clearRect(sd.x, 0, sd.w, H);
x.globalCompositeOperation = 'lighter';
}
for (i = 0; i < threads.length; i++) { posThread(threads[i], t, dt); }
var ord = threads.slice().sort(byD);
for (i = 0; i < ord.length; i++) { ord[i].near = i < 3; }
for (i = 0; i < motes.length; i++) {
var m = motes[i], y = (((m.y0 * (H + 40) - m.v * t) % (H + 40)) + H + 40) % (H + 40) - 20;
sd = sides[m.side];
dot(sd.cx, sd.x + 6 + (sd.w - 12) * m.fx + m.ax * Math.sin(m.wx * t + m.ph), y, m.s, m.a * (0.55 + 0.45 * Math.sin(m.tw * t + m.ph * 3)));
}
for (i = 0; i < threads.length; i++) {
th = threads[i]; x = sides[th.side].cx;
x.globalAlpha = 1; x.beginPath(); path(x, th);
x.strokeStyle = th.g0; x.lineWidth = 3.5 + 3 * th.z; x.stroke();
x.strokeStyle = th.g1; x.lineWidth = th.lw; x.stroke();
var ex = th.px[N], ey = th.py[N], ea = th.a * 0.55;
if (th.end === 1) {
dot(x, ex, ey, 4 + 3 * th.z, ea * 0.35);
x.globalAlpha = ea; x.fillStyle = th.col; x.beginPath(); x.arc(ex, ey + 1, 1 + 0.6 * th.z, 0, 6.2832); x.fill();
} else if (th.end === 2) {
var ang = Math.atan2(ey - th.py[N - 1], ex - th.px[N - 1]), r = 3 + 1.5 * th.z, d = th.hookDir;
var ccx = ex - Math.sin(ang) * r * d, ccy = ey + Math.cos(ang) * r * d, a0 = Math.atan2(ey - ccy, ex - ccx);
x.globalAlpha = 1; x.strokeStyle = th.colE; x.lineWidth = th.lw * 0.9; x.beginPath();
x.arc(ccx, ccy, r, a0, a0 + d * 3.6, d < 0); x.stroke();
}
}
sparks(t);
for (i = 0; i < sides.length; i++) { if (sides[i].on) { sides[i].cx.globalCompositeOperation = 'source-over'; sides[i].cx.globalAlpha = 1; } }
}
/* 30 кадров/с, и при прокрутке тоже; после 15 с без действий или если кадры сами идут > 45 мс — 15 */
function loop(ts) {
raf = 0;
if (!on || hid) { return; }
raf = G.raf(loop);
ts = performance.now();
if (prevTs) { var dd = Math.min(250, ts - prevTs); ema += (dd - ema) * 0.05; } prevTs = ts;
FRAME = ema > 45 || ts - lastInput > 15000 ? 1000 / 15 - 2 : ema < 24 ? 1000 / 30 - 2 : FRAME;
if (ts - lastDraw < FRAME) { return; }
var t = now(), dt = prevT ? Math.min(0.1, t - prevT) : 0;
lastDraw = ts; prevT = t;
var p0 = performance.now(); draw(t, dt); var p1 = performance.now();
if (G.stat) { stA.push(p1 - p0); stN++; if (p1 - stT > 250) { G.stat(stA, stN); stA = []; stN = 0; stT = p1; } }
}
function run(v) {
on = v && !still;
if (on && !hid && !raf && sides.length) { prevT = 0; prevTs = 0; lastInput = performance.now(); raf = G.raf(loop); }
if (!on && raf) { G.caf(raf); raf = 0; }
}
return {
msg: function (d) {
if (d.g) { setGeom(d.g); if (still) { draw(now(), 0); } else if (on && !hid && !raf) { run(true); } }
if (d.still) { still = true; on = false; if (sides.length) { draw(now(), 0); } }
if (d.inp) { lastInput = performance.now(); }
if (d.m) { mouse.x = d.m[0]; mouse.y = d.m[1]; mouse.in = d.m[2]; lastInput = performance.now(); }
if ('hid' in d) { hid = d.hid; if (hid && raf) { G.caf(raf); raf = 0; } else { run(on); } }
if ('run' in d) { run(d.run); }
},
threads: function () { return threads; }
};
}
var cvs = [], E = null, WK = null, mode = '', on = false, last = '', sides = [], tmr = 0, geomMsg = null, runMsg = null, lastPost = 0, mIn = false;
var api = window.lotmBgt = { mode: '', frames: 0, threads: function () { return E ? E.threads() : []; } }; /* для отладки */
function onStat(a, n) { api.frames += n; if (window.lotmBgtStat) { for (var i = 0; i < a.length; i++) { window.lotmBgtStat.push(a[i]); } } }
function post(d) { if (WK) { WK.postMessage(d); } else if (E) { E.msg(d); } }
function mkCanvas(i) {
var c = document.createElement('canvas'); c.className = 'lotm-bgt lotm-bgt-' + (i ? 'r' : 'l'); c.setAttribute('aria-hidden', 'true');
c.style.width = '0px';
return c;
}
function mainCore() {
mode = api.mode = 'main';
E = core({ seed: SEED, start: START, cvs: cvs, raf: function (f) { return requestAnimationFrame(f); }, caf: function (h) { cancelAnimationFrame(h); }, stat: onStat });
}
function engine() {
var src = 'var E;onmessage=function(e){var d=e.data;if(d.init){var R=self.requestAnimationFrame?function(f){return requestAnimationFrame(f)}:function(f){return setTimeout(function(){f(performance.now())},33)},C=self.cancelAnimationFrame?function(h){cancelAnimationFrame(h)}:clearTimeout;' +
'E=(' + core.toString() + ')({seed:d.seed,start:d.start,cvs:d.cvs,raf:R,caf:C,stat:function(a,n){postMessage({st:a,n:n})}});postMessage({ready:1})}else if(E){E.msg(d)}};';
try {
if (!window.Worker || !window.OffscreenCanvas || !cvs[0].transferControlToOffscreen || !window.Blob || !window.URL) { throw 0; }
var url = URL.createObjectURL(new Blob([src], { type: 'text/javascript' })), ready = false;
WK = new Worker(url);
var off = [cvs[0].transferControlToOffscreen(), cvs[1].transferControlToOffscreen()];
WK.onmessage = function (e) { var d = e.data; if (d.ready) { ready = true; URL.revokeObjectURL(url); } else if (d.st) { onStat(d.st, d.n); } };
WK.onerror = function () { if (!ready) { fallback(); } };
WK.postMessage({ init: 1, seed: SEED, start: START, cvs: off }, off);
mode = api.mode = 'worker';
} catch (e) { WK = null; if (cvs[0].transferControlToOffscreen && e !== 0) { fallback(); } else { mainCore(); } }
}
function fallback() { /* Worker не поднялся (например, blob: запрещён) — новые canvas и ядро в главном потоке */
if (WK) { try { WK.terminate(); } catch (e) {} WK = null; }
cvs.forEach(function (c, i) { var n = mkCanvas(i); n.style.cssText = c.style.cssText; if (c.parentNode) { c.parentNode.replaceChild(n, c); } cvs[i] = n; });
mainCore();
if (geomMsg) { E.msg(geomMsg); }
if (mqRM.matches) { E.msg({ still: 1 }); } else if (runMsg) { E.msg(runMsg); }
}
function geom() {
var main = document.querySelector('.mw-body'), hdr = document.querySelector('.citizen-header');
if (!main) { return false; }
var r = main.getBoundingClientRect(), vw = document.documentElement.clientWidth, vh = window.innerHeight, hl = 0, hc = hdr && hdr.getBoundingClientRect();
if (hc && hc.height > vh * 0.6 && hc.left < 1) { hl = Math.round(hc.right); }
var pl = Math.round(r.left), pr = Math.round(r.right), key = [hl, pl, pr, vw, vh].join();
if (key === last) { return false; }
last = key;
sides = [[hl, Math.max(0, pl - hl)], [pr, Math.max(0, vw - pr)]];
sides.forEach(function (s, i) { var c = cvs[i].style; c.left = s[0] + 'px'; c.width = (s[1] >= 40 ? s[1] : 0) + 'px'; });
geomMsg = { g: { s: sides, W: vw, H: vh, DPR: Math.min(1.5, window.devicePixelRatio || 1) } };
post(geomMsg);
return true;
}
function alive() { return cvs[0].getClientRects().length > 0 && !document.documentElement.classList.contains('lotm-lite'); }
function tick() { /* геометрия и «не скрыт ли canvas» — раз в секунду, пока нити идут */
tmr = 0;
if (!on || document.hidden) { return; }
geom();
if (!alive()) { stop(); return; }
tmr = setTimeout(tick, 1000);
}
function stop() { on = false; runMsg = { run: false }; post(runMsg); if (tmr) { clearTimeout(tmr); tmr = 0; } }
function start() {
if (!cvs.length || mqRM.matches) { return; }
if (!alive()) { if (on) { stop(); } return; }
on = true; geom(); runMsg = { run: true }; post(runMsg);
if (!tmr && !document.hidden) { tmr = setTimeout(tick, 1000); }
}
function poke() { var t = performance.now(); if (t - lastPost > 1000) { lastPost = t; post({ inp: 1 }); } }
function init() {
if (cvs.length || !mqDesk.matches || !document.querySelector('.mw-body') || document.documentElement.classList.contains('lotm-lite')) { return; }
cvs = [mkCanvas(0), mkCanvas(1)];
document.body.insertBefore(cvs[1], document.body.firstChild);
document.body.insertBefore(cvs[0], document.body.firstChild);
engine();
if (document.hidden) { post({ hid: true }); }
geom();
if (mqRM.matches) { post({ still: 1 }); return; } /* без движения — один статичный кадр */
if (FRESH) { /* проявление — только в начале сессии: CSS-переход opacity (композитор) */
cvs.forEach(function (c) { c.style.opacity = '0'; c.style.transition = 'opacity 3s linear'; });
requestAnimationFrame(function () { requestAnimationFrame(function () { cvs.forEach(function (c) { c.style.opacity = ''; }); }); });
setTimeout(function () { cvs.forEach(function (c) { c.style.transition = ''; }); }, 3600);
}
window.addEventListener('scroll', poke, { passive: true });
window.addEventListener('keydown', poke, { passive: true });
window.addEventListener('pointerdown', poke, { passive: true });
window.addEventListener('lotm-lite', function (e) { if (e.detail) { stop(); } });
window.addEventListener('pointermove', function (e) {
var x = e.clientX, inn = false;
for (var i = 0; i < sides.length; i++) { if (sides[i][1] >= 40 && x >= sides[i][0] && x < sides[i][0] + sides[i][1]) { inn = true; } }
inn = inn && e.pointerType !== 'touch';
if (inn || mIn) { post({ m: [x, e.clientY, inn] }); lastPost = performance.now(); } else { poke(); }
mIn = inn;
}, { passive: true });
var out = function () { if (mIn) { mIn = false; post({ m: [-1e4, -1e4, false] }); } };
document.addEventListener('pointerleave', out);
window.addEventListener('blur', out);
window.addEventListener('resize', function () { if (on) { geom(); } else { start(); } }, { passive: true });
document.addEventListener('visibilitychange', function () {
post({ hid: document.hidden });
if (!document.hidden) { start(); } else if (tmr) { clearTimeout(tmr); tmr = 0; }
});
if (window.MutationObserver) { new MutationObserver(function () { if (!on) { last = ''; start(); } }).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); }
start();
}
if (mqDesk.addEventListener) { mqDesk.addEventListener('change', function () { init(); start(); }); }
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }
})();
/* ===== QFIX (wave30): подсказка «Нажмите, чтобы открыть спойлер» — одна, только при наведении =====
Дополнение к бандлу: вставить В КОНЕЦ MediaWiki:Common.js (после spoilerfix). Постоянный слой
.lotm-sf-layer скрыт в QFIX.css; здесь — одна плашка у курсора над закрытой целью, прижатая к границам
инфобокса/колонки статьи и окна. На сенсорных экранах (нет hover) не показывается: тап сразу открывает. */
( function () {
'use strict';
if ( window.lotmQfPill ) { return; }
window.lotmQfPill = true;
var HINT = 'Нажмите, чтобы открыть спойлер';
var SEL = '.spoilerblur:not(.lotm-revealed), .lotm-sf-closed';
var pill = null, cur = null, lx = 0, ly = 0, raf = 0;
function ensure() {
if ( pill && pill.isConnected ) { return pill; }
pill = document.createElement( 'span' );
pill.className = 'lotm-qf-pill';
pill.setAttribute( 'aria-hidden', 'true' );
pill.textContent = HINT;
document.body.appendChild( pill );
return pill;
}
function target( el ) {
return el && el.closest ? el.closest( SEL ) : null;
}
function hide() { cur = null; if ( pill ) { pill.classList.remove( 'is-on' ); } }
function place() {
raf = 0;
if ( !cur || !cur.isConnected || !cur.matches( SEL ) ) { hide(); return; }
var p = ensure(), box = cur.closest( '.portable-infobox, aside, table.infobox' ) || cur.closest( '.mw-parser-output' ) || document.body;
var b = box.getBoundingClientRect(), vw = document.documentElement.clientWidth, vh = window.innerHeight;
var L = Math.max( 4, b.left + 2 ), R = Math.min( vw - 4, b.right - 2 );
p.style.maxWidth = Math.max( 120, R - L ) + 'px';
var w = p.offsetWidth, h = p.offsetHeight;
var x = Math.min( Math.max( lx - w / 2, L ), Math.max( L, R - w ) );
var y = ly - h - 14;
if ( y < 4 ) { y = ly + 18; }
y = Math.min( Math.max( 4, y ), vh - h - 4 );
p.style.transform = 'translate(' + Math.round( x ) + 'px,' + Math.round( y ) + 'px)';
p.classList.add( 'is-on' );
}
function sched() { if ( !raf ) { raf = requestAnimationFrame( place ); } }
document.addEventListener( 'pointermove', function ( e ) {
if ( e.pointerType && e.pointerType !== 'mouse' && e.pointerType !== 'pen' ) { return; }
var t = target( e.target );
if ( !t ) { if ( cur ) { hide(); } return; }
cur = t; lx = e.clientX; ly = e.clientY; sched();
}, { passive: true } );
document.addEventListener( 'pointerdown', hide, true );
document.addEventListener( 'click', function () { setTimeout( function () { if ( cur && !cur.matches( SEL ) ) { hide(); } }, 0 ); }, true );
document.addEventListener( 'mouseleave', hide );
window.addEventListener( 'scroll', hide, { passive: true } );
window.addEventListener( 'blur', hide );
/* клавиатура: фокус на закрытой цели — подсказка над её центром */
document.addEventListener( 'focusin', function ( e ) {
var t = target( e.target );
if ( !t || !t.matches( ':focus-visible' ) ) { return; }
var r = t.getBoundingClientRect(); cur = t; lx = r.left + r.width / 2; ly = r.top; sched();
} );
document.addEventListener( 'focusout', function ( e ) { if ( cur && e.target === cur ) { hide(); } } );
}() );
/* (V1) рамка-цитата, весь видимый текст которой скрыт (свёрнутый спойлер) — схлопнуть, а не показывать пустую коробку */
( function () {
'use strict';
if ( window.lotmQfEmpty ) { return; }
window.lotmQfEmpty = true;
var SEL = '.mw-parser-output .ramki, #content blockquote.pull-quote, #content .blockquote.blockwrapper', raf = 0, mo;
function run() {
raf = 0; mo.disconnect();
[].forEach.call( document.querySelectorAll( SEL ), function ( q ) {
var was = q.classList.contains( 'lotm-qf-empty' );
if ( was ) { q.classList.remove( 'lotm-qf-empty' ); }
if ( !q.getClientRects().length ) { return; } // в скрытой вкладке — не трогаем
var txt = ( q.innerText || '' ).replace( /[\s«»"“”—–-]+/g, '' );
var media = q.querySelector( 'img, video, svg, .spoilerblur, .lotm-sf-closed, .mw-collapsed' );
if ( !txt && !( media && media.getClientRects().length ) ) { q.classList.add( 'lotm-qf-empty' ); }
} );
mo.takeRecords(); watch();
}
function watch() { mo.observe( document.body, { attributes: true, attributeFilter: [ 'class', 'style' ], subtree: true, childList: true } ); }
function sched() { if ( !raf ) { raf = requestAnimationFrame( run ); } }
var lotmQfOwn = /^(lotm-(sf-closed|fog-live|fog-anim|fog-gone|tilting|press|offscreen|qf-empty)|lotm-leaving|lotm-back)$/;
mo = new MutationObserver( function ( recs ) {
for ( var i = 0; i < recs.length; i++ ) {
var r = recs[ i ], t = r.target;
if ( !t.closest || t.closest( '.lotm-sf-layer, .lotm-bgt, .lotm-glow' ) ) { continue; }
if ( r.type === 'attributes' && r.attributeName === 'style' && !t.closest( SEL ) ) { continue; }
if ( r.type === 'attributes' && r.attributeName === 'class' && t === document.documentElement ) { continue; }
sched(); return;
}
} ); watch();
if ( window.mw && mw.hook ) { mw.hook( 'wikipage.content' ).add( function () { setTimeout( sched, 450 ); } ); }
setTimeout( sched, 500 );
}() );
/* ===== ANIM (волна 42): затухание статьи при уходе по внутренней ссылке =====
* Клик по внутренней ссылке того же окна (без Ctrl/Cmd/Shift/Alt, левой кнопкой) →
* html.lotm-leaving (контент гаснет за 180 мс, CSS в ANIM.css) → переход.
* Не трогает: модификаторы и средний клик, target≠_self, download, якоря той же страницы,
* ссылки-кнопки (role=button, переключатели спойлеров/свёрток), формы, редактор, файлы,
* другие домены, уже обработанные кем-то клики (defaultPrevented — Citizen, превью, спойлеры).
* Возврат «назад» (bfcache, pageshow.persisted) — класс снимается, контент возвращается.
* prefers-reduced-motion: ничего не перехватывается. */
( function () {
if ( window.lotmPageLeave ) { return; }
window.lotmPageLeave = 1;
var root = document.documentElement, DUR = 180, timer = 0, safety = 0;
var mqRM = window.matchMedia ? window.matchMedia( '(prefers-reduced-motion: reduce)' ) : { matches: false };
var FILE = /\.(png|jpe?g|gif|webp|svg|avif|pdf|zip|7z|rar|mp3|ogg|mp4|webm|txt|json)$/i;
function reset( soft ) {
clearTimeout( timer ); clearTimeout( safety ); timer = safety = 0;
if ( !root.classList.contains( 'lotm-leaving' ) ) { return; }
if ( soft ) {
root.classList.add( 'lotm-back' );
root.classList.remove( 'lotm-leaving' );
setTimeout( function () { root.classList.remove( 'lotm-back' ); }, 300 );
} else {
root.classList.remove( 'lotm-leaving', 'lotm-back' );
}
}
function eligible( e ) {
if ( e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey ) { return null; }
if ( mqRM.matches || root.classList.contains( 'lotm-leaving' ) ) { return null; }
var t = e.target;
if ( !t || !t.closest ) { return null; }
var a = t.closest( 'a[href]' );
if ( !a || a.closest( 'form, [contenteditable], .ve-ce-surface, .ve-init-target' ) ) { return null; }
if ( a.hasAttribute( 'download' ) || a.getAttribute( 'role' ) === 'button' ) { return null; }
var tgt = ( a.getAttribute( 'target' ) || '' ).toLowerCase();
if ( tgt && tgt !== '_self' ) { return null; }
var raw = a.getAttribute( 'href' ) || '';
if ( !raw || raw.charAt( 0 ) === '#' || /^\s*(javascript|mailto|tel|data):/i.test( raw ) ) { return null; }
if ( /(^|\s)(mw-collapsible-toggle|mw-customtoggle|mw-selflink|new)(\s|$)/.test( a.className ) || /mw-customtoggle/.test( a.className ) ) { return null; }
var u;
try { u = new URL( a.href, location.href ); } catch ( err ) { return null; }
if ( u.origin !== location.origin || !/^https?:$/.test( u.protocol ) ) { return null; }
if ( u.pathname === location.pathname && u.search === location.search ) { return null; } /* якорь / та же страница */
if ( FILE.test( u.pathname ) || /^\/(images|load\.php|api\.php|rest\.php)/.test( u.pathname ) ) { return null; }
if ( /[?&]action=(raw|render)/.test( u.search ) ) { return null; }
return u.href;
}
window.addEventListener( 'click', function ( e ) { /* window, а не document: срабатывает последним, после всех обработчиков на document */
var href = eligible( e );
if ( !href ) { return; }
e.preventDefault();
root.classList.remove( 'lotm-back' );
root.classList.add( 'lotm-leaving' );
timer = setTimeout( function () { location.href = href; }, DUR );
/* если переход не случился (скачивание, 204, отмена) — вернуть контент через 4 с */
safety = setTimeout( function () { reset( true ); }, 4000 );
} );
window.addEventListener( 'pageshow', function ( e ) { reset( !!e.persisted ); } );
window.addEventListener( 'pagehide', function () { clearTimeout( safety ); } );
document.addEventListener( 'visibilitychange', function () {
/* вкладку спрятали во время затухания (например, Ctrl+клик по превью) — не оставлять прозрачной */
if ( document.visibilityState === 'visible' && root.classList.contains( 'lotm-leaving' ) && !timer ) { reset( true ); }
} );
}() );
/* GLOW_final — «Багровая ночь» (усиленная) + «Затмение». Пара к GLOW_final.css. Тема/ширину решает CSS.
Одна система времени на сессию: sessionStorage 'lotmGlow' = {t0: старт (мс), s: зерно}.
- Налёт: CSS-анимация 20 с, фаза = (сейчас − t0) через --lotm-glow-t (отрицательный animation-delay).
- Затмения: детерминированное расписание из зерна: первое через 15–20 с, дальше интервалы 60–90 с, длительность 3–4 с.
Каждое затмение — Web Animation (opacity, композитор) с currentTime = сейчас − начало события, поэтому
при переходе на другую статью / возврате во вкладку оно продолжается с нужной фазы и не повторяется.
Между событиями — один setTimeout до следующего (скрытая вкладка: таймер дросселируется, кадров нет).
- ?glowdemo=1 — затмения каждые 15 с (запоминается в сессии; ?glowdemo=0 — выключить).
- prefers-reduced-motion — статичный налёт, затмений нет. */
(function () {
'use strict';
if (window.lotmGlow) { return; }
var K = 'lotmGlow', st = null, demo = false, el, ecl, rim, tl0 = 0, base = 0, timer = 0, cur = -1, anims = [];
try { st = JSON.parse(sessionStorage.getItem(K) || 'null'); } catch (e) {}
if (!st || !st.t0) { st = { t0: Date.now(), s: Math.floor(Math.random() * 4294967296) }; try { sessionStorage.setItem(K, JSON.stringify(st)); } catch (e) {} }
try {
var m = /[?&]glowdemo=([01])/.exec(location.search);
if (m) { sessionStorage.setItem('lotmGlowDemo', m[1]); }
demo = sessionStorage.getItem('lotmGlowDemo') === '1';
} catch (e) { demo = /[?&]glowdemo=1/.test(location.search); }
var mqRM = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : { matches: false };
/* детерминированное псевдослучайное число [0,1) для события k (mulberry32-хэш от зерна и k) */
function rnd(k, j) {
var a = (st.s ^ Math.imul(k + 1, 0x9E3779B1) ^ Math.imul(j + 1, 0x85EBCA77)) >>> 0;
a = (a + 0x6D2B79F5) >>> 0; var t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
/* событие k: {k, a: начало (с от t0), d: длительность (с)} */
function ev(k) {
if (demo) { return { k: k, a: 5 + 15 * k, d: 11 }; }
var a = 15 + 5 * rnd(0, 0);
for (var i = 1; i <= k; i++) { a += 45 + 15 * rnd(i, 0); }
return { k: k, a: a, d: 10 + 3 * rnd(k, 1) };
}
/* первое событие, которое ещё не закончилось к моменту t (с) */
function find(t) {
if (demo) { var j = Math.max(0, Math.floor((t - 8.5) / 15)), e = ev(j); while (e.a + e.d <= t) { e = ev(++j); } return e; }
var a = 15 + 5 * rnd(0, 0), k = 0, d = 10 + 3 * rnd(0, 1);
while (a + d <= t) { k++; a += 45 + 15 * rnd(k, 0); d = 10 + 3 * rnd(k, 1); }
return { k: k, a: a, d: d };
}
function now() { return (Date.now() - st.t0) / 1000; }
function tl() { return (document.timeline && document.timeline.currentTime != null ? document.timeline.currentTime : performance.now()) / 1000; }
/* плавно темнеет и краснеет (≈35 %), держится (≈30 %), плавно уходит (≈35 %); подсвет чуть запаздывает */
var KE = [{ opacity: 0, offset: 0, easing: 'ease-in-out' }, { opacity: 1, offset: 0.3 }, { opacity: 1, offset: 0.7, easing: 'ease-in-out' }, { opacity: 0, offset: 1 }];
var KR = [{ opacity: 0, offset: 0, easing: 'ease-in-out' }, { opacity: 0, offset: 0.08, easing: 'ease-in-out' }, { opacity: 1, offset: 0.42 }, { opacity: 0.85, offset: 0.6, easing: 'ease-in-out' }, { opacity: 0, offset: 1 }];
function play(e, t) {
if (cur === e.k || !ecl.animate) { return; }
cur = e.k; stop();
var ms = e.d * 1000, at = Math.max(0, (t - e.a) * 1000);
anims = [ecl.animate(KE, { duration: ms, fill: 'none' }), rim.animate(KR, { duration: ms, fill: 'none' })];
anims.forEach(function (a) { a.currentTime = at; });
}
function stop() { anims.forEach(function (a) { try { a.cancel(); } catch (e) {} }); anims = []; }
function plan() {
clearTimeout(timer); timer = 0;
if (mqRM.matches || document.hidden) { stop(); cur = -1; return; }
var t = now(), e = find(t);
if (t >= e.a) { play(e, t); e = find(e.a + e.d + 0.001); }
timer = setTimeout(plan, Math.max(50, (e.a - now()) * 1000));
}
function resync() {
var want = now(), have = (tl() - tl0) + base;
if (Math.abs(want - have) > 0.25) { base += want - have; el.style.setProperty('--lotm-glow-t', base.toFixed(3)); }
}
function init() {
if (el || !document.body) { return; }
el = document.createElement('div');
el.className = 'lotm-glow' + (demo ? ' lotm-glow--demo' : '');
el.setAttribute('aria-hidden', 'true');
el.innerHTML = '<div class="lotm-glow__wash"></div><div class="lotm-glow__eclipse"></div><div class="lotm-glow__rim"></div>';
ecl = el.children[1]; rim = el.children[2];
tl0 = tl(); base = now(); el.style.setProperty('--lotm-glow-t', base.toFixed(3));
document.body.insertBefore(el, document.body.firstChild);
plan(); /* до первой отрисовки: если затмение уже идёт — сразу с нужной фазы, без вспышки */
window.addEventListener('pageshow', function (e) { if (e.persisted) { resync(); cur = -1; plan(); } });
document.addEventListener('visibilitychange', function () { if (!document.hidden) { resync(); cur = -1; } plan(); });
if (mqRM.addEventListener) { mqRM.addEventListener('change', plan); }
}
window.lotmGlow = { variant: 'final', now: now, next: function () { return find(now()); }, demo: demo };
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }
})();
/* ===== SEALFX (wave30): разметка значков Путей для подсветки (замена A3) =====
* Только МЕЛКИЕ значки (ширина ≤ 48px: у ссылок, фишки, таблицы, навбоксы, вкладки карточек).
* Без обработчиков событий: один проход по новому контенту ставит обёртке значка
* class="lotm-sfx" и data-sfx="<путь>" (из имени файла …/Door_Symbol2.png → door).
* Цвет и движение задаёт CSS по data-sfx — ни getComputedStyle, ни mouseover на документ. */
( function () {
var RX = /\/(?:\d+px-)?([A-Za-z][A-Za-z_-]*?)_Symbol\d*\.\w+(?:$|[?#])/;
function tag( root ) {
var imgs = root.querySelectorAll( 'img[src*="_Symbol"]' ), i, img, w, m;
for ( i = 0; i < imgs.length; i++ ) {
img = imgs[ i ]; w = img.parentNode;
if ( !w || w.nodeType !== 1 || w.hasAttribute( 'data-sfx' ) ) { continue; }
if ( ( +img.getAttribute( 'width' ) || img.width || 0 ) > 48 ) { continue; } /* крупные символы (≥ ~64px) — зона BIGSEAL.css */
if ( w.tagName !== 'A' && !( w.tagName === 'SPAN' && w.children.length === 1 ) ) { continue; }
m = RX.exec( img.getAttribute( 'src' ) || '' );
w.setAttribute( 'data-sfx', m ? m[ 1 ].toLowerCase().replace( /_/g, '-' ) : 'x' );
w.classList.add( 'lotm-sfx' );
}
}
mw.hook( 'wikipage.content' ).add( function ( $c ) {
var root = ( $c && $c[ 0 ] ) || document;
if ( root.querySelectorAll ) { tag( root ); }
} );
}() );
/* POPFIX: превью ссылок — текст обрезается целыми строками: по концу предложения, иначе по слову + «…» */
(function () {
'use strict';
if (window.lotmPopfix) return; window.lotmPopfix = 1;
var SENT = /[.!?…][»"”)\]]*(?=\s+[«"„(\[A-ZА-ЯЁ0-9—–-]|\s*$)/g;
function texts(root) {
var w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT), a = [], n;
while ((n = w.nextNode())) a.push(n);
return a;
}
/* оставить первые k символов текста (разметку сохраняем), ell — добавить «…» */
function cut(ex, orig, k, ell) {
ex.innerHTML = orig;
var tn = texts(ex), pos = 0, done = false;
tn.forEach(function (n) {
var len = n.data.length;
if (done) { n.data = ''; return; }
if (pos + len >= k) {
n.data = n.data.slice(0, k - pos);
if (ell) n.data = n.data.replace(/[\s,;:—–\-(«"„]+$/, '') + '…';
done = true;
}
pos += len;
});
Array.prototype.slice.call(ex.querySelectorAll('*')).reverse().forEach(function (e) {
if (!/^(BR|IMG)$/.test(e.tagName) && !e.textContent.trim() && !e.querySelector('img')) e.remove();
});
}
/* высота текста от верха области содержимого (верх пересчитываем: у перевёрнутого вверх превью он сдвигается) */
function textH(ex, pt) {
var r = document.createRange(); r.selectNodeContents(ex);
var rs = r.getClientRects(), b = 0, i, t = ex.getBoundingClientRect().top + pt;
for (i = 0; i < rs.length; i++) if (rs[i].height) b = Math.max(b, rs[i].bottom);
return b - t;
}
function fix(ex) {
if (ex.getAttribute('data-lotm-popfix')) return;
var pop = ex.closest('.mwe-popups');
if (!pop || pop.className.indexOf('mwe-popups-type-page') < 0) return;
ex.setAttribute('data-lotm-popfix', '1');
var cs = getComputedStyle(ex), lh = parseFloat(cs.lineHeight) || 1.5 * parseFloat(cs.fontSize) || 21,
pt = parseFloat(cs.paddingTop) || 0, pb = parseFloat(cs.paddingBottom) || 0,
box = ex.getBoundingClientRect(),
mh = parseFloat(cs.maxHeight), /* is-not-tall: высота по тексту, до max-height */
avail = mh ? mh - (cs.boxSizing === 'border-box' ? pt + pb : 0) : box.height - pt - pb;
if (textH(ex, pt) <= avail + 1) return; /* текст помещается целиком */
var N = Math.floor((avail + 1) / lh);
if (N < 1) return;
var lim = N * lh + 1, orig = ex.innerHTML, full = ex.textContent, sc = [], wc = [], m, lo, hi, mid, best;
ex.style.maxHeight = (N * lh + pt + pb) + 'px'; /* видимая область — ровно N строк */
try {
SENT.lastIndex = 0;
while ((m = SENT.exec(full))) sc.push(m.index + m[0].length);
full.replace(/[^\s—–-](?=[\s—–-])/g, function (_, i) { wc.push(i + 1); });
best = -1; lo = 0; hi = sc.length - 1;
while (lo <= hi) { mid = (lo + hi) >> 1; cut(ex, orig, sc[mid], false); if (textH(ex, pt) <= lim) { best = mid; lo = mid + 1; } else hi = mid - 1; }
if (best >= 0) {
cut(ex, orig, sc[best], false);
/* конец предложения, если теряем не больше четверти окна (минимум 1 строку) */
var used = Math.round(textH(ex, pt) / lh);
if (N - used <= Math.max(1, Math.floor(N / 4))) return;
}
best = -1; lo = 0; hi = wc.length - 1;
while (lo <= hi) { mid = (lo + hi) >> 1; cut(ex, orig, wc[mid], true); if (textH(ex, pt) <= lim) { best = mid; lo = mid + 1; } else hi = mid - 1; }
if (best >= 0) cut(ex, orig, wc[best], true); else ex.innerHTML = orig;
} catch (e) { ex.innerHTML = orig; }
}
function scan(root) {
if (!root.querySelectorAll) return;
if (root.matches && root.matches('.mwe-popups-extract')) fix(root);
root.querySelectorAll('.mwe-popups-extract').forEach(fix);
}
function start() {
new MutationObserver(function (ms) {
var nodes = [];
ms.forEach(function (m) { m.addedNodes.forEach(function (n) { if (n.nodeType === 1) nodes.push(n); }); });
if (nodes.length) requestAnimationFrame(function () { nodes.forEach(scan); });
}).observe(document.body, { childList: true });
scan(document.body);
}
if (document.body) start(); else document.addEventListener('DOMContentLoaded', start);
}());
/* BIGSEAL (wave30 v2): крупные символы Путей (≥64px) — обёртка .lotm-bigseal, цвет Пути, силуэт для сияния, тап (1 с) на телефоне */
(function () {
var C = { Fool: 'lom', Error: 'lom', Door: 'lom', Visionary: 'ga', 'Hanged Man': 'ga', Sun: 'ga', Tyrant: 'ga', 'White Tower': 'ga',
Darkness: 'ed', Death: 'ed', 'Twilight Giant': 'ed', 'Red Priest': 'cod', Demoness: 'cod', Paragon: 'dok', Hermit: 'dok',
'Wheel of Fortune': 'kol', Moon: 'goo', Mother: 'goo', Chained: 'fod', Abyss: 'fod', 'Black Emperor': 'ta', Justiciar: 'ta',
'Eternal Aeon': 'coi', 'Chaos Primogenitor': 'mgod', Patriarch: 'mtod', 'Chaos Mist': 'soc', 'Eternal Edict': 'gof',
Condenser: 'sd', 'Tail-Devourer': 'ph', 'Sublunary Eye': 'hdo', Everlasting: 'ir', 'Second Law': 'ir', 'Monarch of Decay': 'mod' };
var MIN = 64;
function mark(img) {
var w = img.parentNode;
if (!w || w.nodeType !== 1 || w.classList.contains('lotm-bigseal') || w.hasAttribute('data-sfx')) { return; } /* data-sfx — мелкий значок SEALFX */
var m = /\/([^\/]+?)_Symbol\d*\.(?:png|webp|svg|jpe?g)/i.exec(decodeURIComponent(img.getAttribute('src') || ''));
var k = m && C[m[1].replace(/_/g, ' ')];
w.classList.add('lotm-bigseal');
if (k) { w.style.setProperty('--lotm-seal', 'var(--color-' + k + ')'); }
w.style.setProperty('--lotm-seal-img', 'url("' + (img.currentSrc || img.src).replace(/["\\]/g, '') + '")');
if (!w.hasAttribute('data-lotm-seal-tap')) {
w.setAttribute('data-lotm-seal-tap', '');
var t;
w.addEventListener('touchstart', function () {
clearTimeout(t); w.classList.add('lotm-seal-tap');
t = setTimeout(function () { w.classList.remove('lotm-seal-tap'); }, 1000);
}, { passive: true });
}
}
function run($c) {
var root = ($c && $c[0]) || document;
var imgs = root.querySelectorAll('img[src*="Symbol"].mw-file-element'), small = [];
Array.prototype.forEach.call(imgs, function (img) {
if ((+img.getAttribute('width') || 0) >= MIN) { mark(img); } else { small.push(img); }
});
if (!small.length) { return; }
(window.requestIdleCallback || function (f) { return setTimeout(f, 300); })(function () {
small.forEach(function (img) { if (img.offsetWidth >= MIN) { mark(img); } });
});
}
mw.hook('wikipage.content').add(run);
}());
/* ===== QUNIFY — единый вид цитатных рамок: убрать дублирующие внешние «…», снять «— » в подписи молитв,
пометить длинные тексты (см. QUNIFY.css) ===== */
( function () {
var SKIP = '.pull-quote__source,.ramki-caption,.refpopups-custom-content,sup,.mw-ref,.blockquote__source';
function texts( box ) {
var out = [], w = document.createTreeWalker( box, NodeFilter.SHOW_TEXT, null ), n;
while ( ( n = w.nextNode() ) ) {
if ( n.parentElement && n.parentElement.closest( SKIP ) ) { continue; }
if ( box.classList.contains( 'blockwrapper' ) && n.parentElement.closest( '.blockwrapper>div:last-child:not(.poem)' ) ) { continue; }
if ( n.nodeValue.trim() ) { out.push( n ); }
}
return out;
}
function run( box ) {
if ( box.getAttribute( 'data-qu' ) ) { return; }
box.setAttribute( 'data-qu', '1' );
if ( box.querySelector( 'div.pull-quote__text' ) ) { return; } /* диалог — без кавычек */
var ns = texts( box ), all = ns.map( function ( n ) { return n.nodeValue; } ).join( '' ).trim();
if ( all.length > 380 ) { box.classList.add( 'lotm-qu-long' ); }
if ( !ns.length || !/^[«„"“]/.test( all ) ) { return; }
var one = /^«[^«»]*»[.!?…]*$/.test( all ) || /^[„“][^„“”]*[“”][.!?…]*$/.test( all ) || /^"[^"]*"[.!?…]*$/.test( all );
if ( !one ) { box.classList.add( 'lotm-qu-noqm' ); return; }
var a = ns[ 0 ], z = ns[ ns.length - 1 ];
a.nodeValue = a.nodeValue.replace( /^(\s*)[«„“"]/, '$1' );
z.nodeValue = z.nodeValue.replace( /[»“”"]([.!?…]*\s*)$/, '$1' );
box.classList.add( 'lotm-qu-g' );
}
function cap( c ) {
var n = c.firstChild;
while ( n && n.nodeType === 3 && !n.nodeValue.trim() ) { n = n.nextSibling; }
if ( n && n.nodeType === 3 ) { n.nodeValue = n.nodeValue.replace( /^\s*[—–-]\s*/, '' ); }
}
mw.hook( 'wikipage.content' ).add( function ( $c ) {
var root = ( $c && $c[ 0 ] ) || document;
if ( !root.querySelectorAll ) { return; }
[].forEach.call( root.querySelectorAll( 'blockquote.pull-quote, .blockquote.blockwrapper, .ramki.ramki--prayer' ), run );
[].forEach.call( root.querySelectorAll( '.ramki.ramki--prayer>.ramki-caption' ), cap );
} );
}() );
/* STARS (wave30): точнее различать слабое устройство (html.lotm-lite) на телефонах.
1) pointer: coarse — «мало ядер» не признак слабости (iOS Safari занижает hardwareConcurrency до 2–4).
2) Замер кадров: старый критерий (медиана > 34 мс, флаг lotm-lite-slow на сутки) ловил 30 fps режима
энергосбережения (33,3 мс + джиттер). Старый флаг игнорируется; новый — медиана > 45 мс И 25-й перцентиль > 34 мс
за 2,5 с (стабильно медленно), флаг lotm-lite-slow2 на сутки. Срабатывание старого замера перепроверяется новым.
3) Звёзды/искры Заглавной (только opacity/transform) в lite больше не выключаются — см. Common.css «STARS».
Остальные признаки (память ≤ 2 ГБ, Save-Data, reduced-motion, режим производительности Citizen) и ручной
?lotmlite=0/1/auto — без изменений. */
(function () {
var L = window.lotmLite, H = document.documentElement, K = 'lotm-lite', ls = null, force = null;
if (!L || L.starsFix) { return; }
L.starsFix = 1;
try { ls = window.localStorage; } catch (e) {}
try {
if (ls && new URLSearchParams(location.search).get('lotmlite') === 'auto') { ls.removeItem(K + '-slow2'); }
force = ls && ls.getItem(K + '-force');
} catch (e) {}
if (force === '1' || force === '0') { return; }
var why = L.why, slow2 = false, nav = navigator, busy = false;
var coarse = !!(window.matchMedia && matchMedia('(pointer: coarse)').matches);
var oldMeasure = !why.length; /* старый замер запланирован только при пустом why */
function drop(r) { var i = why.indexOf(r), had = i >= 0; if (had) { why.splice(i, 1); } if (had && !why.length) { L.set(false); } return had; }
try { ls && ls.removeItem(K + '-slow'); } catch (e) {}
try { slow2 = !!ls && +ls.getItem(K + '-slow2') > Date.now(); } catch (e) {}
if (coarse) { drop('cores'); }
if (slow2) { L.set(true, 'slow-frames'); } else { drop('slow-frames'); }
function measure() {
if (busy || slow2 || document.hidden) { return; }
if (why.length && !(why.length === 1 && why[0] === 'slow-frames')) { return; } /* lite уже по другой причине */
if ((nav.hardwareConcurrency || 0) >= 6 && (!nav.deviceMemory || nav.deviceMemory >= 6)) { drop('slow-frames'); return; }
busy = true;
var arr = [], t0 = 0, prev = 0;
function f(t) {
if (document.hidden) { busy = false; return; }
if (!t0) { t0 = prev = t; } else { arr.push(t - prev); prev = t; }
if (t - t0 < 2500) { requestAnimationFrame(f); return; }
busy = false;
arr.sort(function (a, b) { return a - b; });
var n = arr.length;
if (n > 15 && arr[n >> 1] > 45 && arr[n >> 2] > 34) {
slow2 = true; L.set(true, 'slow-frames');
try { ls && ls.setItem(K + '-slow2', String(Date.now() + 864e5)); } catch (e) {}
} else { drop('slow-frames'); }
}
requestAnimationFrame(f);
}
function later() { setTimeout(function () { if (window.requestIdleCallback) { requestIdleCallback(measure, { timeout: 4000 }); } else { measure(); } }, 1000); }
/* старый замер (порог 34 мс) может сработать позже — перепроверка новым критерием */
window.addEventListener('lotm-lite', function (e) {
if (!e.detail || slow2 || why.indexOf('slow-frames') < 0) { return; }
try { ls && ls.removeItem(K + '-slow'); } catch (x) {}
setTimeout(measure, 500);
});
/* если признаки сняты здесь, старый замер не запланирован — запускаем свой */
if (!oldMeasure && !why.length && !slow2) {
if (document.readyState === 'complete') { later(); } else { window.addEventListener('load', later); }
}
/* возврат из bfcache (Safari): снять паузу скрытой вкладки */
window.addEventListener('pageshow', function () { H.classList.toggle('lotm-tab-hidden', document.hidden); });
})();
/* ===== SPFOG2: строчные спойлеры (.spoilerblur) — тот же туман, что у блоков (.lotm-fog-sky > i.lotm-cloud-1/2/3) =====
* Дописать В КОНЕЦ Common.js. Существующий код не меняется: DOM-слой тот же, что строит fogify() у блоков
* (fogify закрыт в замыкании блоков и переносит детей в div.lotm-fog-body — для строки это не годится, поэтому
* здесь те же 10 строк, но sky — <span> и без обёртки). Классы и жизненный цикл — как у блоков:
* .lotm-fog-ready, .lotm-fog-live (дрейф только в зоне видимости, не в lite), .lotm-fog-anim/.lotm-fog-gone (разлёт при открытии),
* пауза скрытой вкладки — общая html.lotm-fog-paused / html.lotm-tab-hidden.
* Открытие (.lotm-revealed), общий переключатель и подсказку по-прежнему ведёт spoilerfix — здесь только наблюдаем класс. */
( function () {
'use strict';
if ( window.lotmSpFog2 ) { return; }
window.lotmSpFog2 = true;
var H = document.documentElement;
var reduceMotion = !!( window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches );
var multi = []; // строчные спойлеры, разорванные переносом строки: облака обрезаются по фрагментам
var io = ( 'IntersectionObserver' in window ) ? new IntersectionObserver( function ( list ) {
var lite = H.classList.contains( 'lotm-lite' );
list.forEach( function ( e ) {
var el = e.target;
el.classList.toggle( 'lotm-fog-live', e.isIntersecting && !lite && !reduceMotion );
if ( e.isIntersecting && el.lotmSf2Dirty ) { clip( el ); }
} );
}, { rootMargin: '120px 0px' } ) : null;
if ( !window.lotmFogVis ) {
window.lotmFogVis = true;
document.addEventListener( 'visibilitychange', function () { H.classList.toggle( 'lotm-fog-paused', document.hidden ); } );
}
// Перенос строки: abspos-слой у строчного элемента — рамка от первого до последнего фрагмента,
// поэтому для >1 фрагмента обрезаем слой путём из прямоугольников самих фрагментов (только у таких, обычно 0–3 на странице).
function clip( el ) {
el.lotmSf2Dirty = false;
var sky = el.lotmSf2Sky, rs = el.getClientRects(), b, d = '', i, r;
if ( !sky ) { return; }
if ( rs.length < 2 ) { sky.style.clipPath = ''; return; }
b = sky.getBoundingClientRect();
for ( i = 0; i < rs.length; i++ ) {
r = rs[ i ];
if ( r.width < 1 ) { continue; }
d += 'M' + ( r.left - b.left ).toFixed( 1 ) + ' ' + ( r.top - b.top ).toFixed( 1 ) + 'h' + r.width.toFixed( 1 ) + 'v' + r.height.toFixed( 1 ) + 'h' + ( -r.width ).toFixed( 1 ) + 'Z';
}
sky.style.clipPath = d ? 'path("' + d + '")' : '';
}
var pend = 0;
function reclip() {
if ( pend ) { return; }
pend = requestAnimationFrame( function () {
pend = 0;
multi.forEach( function ( el ) { el.lotmSf2Dirty = true; } );
// видимые — сразу, остальные — при входе в зону видимости
var vh = window.innerHeight;
multi.forEach( function ( el ) {
var r = el.getBoundingClientRect();
if ( r.bottom > -200 && r.top < vh + 200 ) { clip( el ); }
} );
} );
}
function watch( el ) {
var last = el.classList.contains( 'lotm-revealed' ), goneTimer = 0, animTimer = 0;
if ( last ) { el.classList.add( 'lotm-fog-gone' ); }
new MutationObserver( function () {
var open = el.classList.contains( 'lotm-revealed' );
if ( open === last ) { return; }
last = open;
clearTimeout( goneTimer ); clearTimeout( animTimer );
el.classList.add( 'lotm-fog-anim' );
animTimer = setTimeout( function () { el.classList.remove( 'lotm-fog-anim' ); }, 1800 );
if ( !open ) { el.classList.remove( 'lotm-fog-gone' ); if ( el.lotmSf2Dirty ) { clip( el ); } }
else { goneTimer = setTimeout( function () { if ( el.classList.contains( 'lotm-revealed' ) ) { el.classList.add( 'lotm-fog-gone' ); } }, 1800 ); }
} ).observe( el, { attributes: true, attributeFilter: [ 'class' ] } );
}
function fogifyInline( el ) {
if ( el.classList.contains( 'lotm-fog-ready' ) ) { return; }
var sky = document.createElement( 'span' ), i, c;
sky.className = 'lotm-fog-sky';
sky.setAttribute( 'aria-hidden', 'true' );
for ( i = 1; i <= 3; i++ ) {
c = document.createElement( 'i' );
c.className = 'lotm-cloud lotm-cloud-' + i;
sky.appendChild( c );
}
el.appendChild( sky );
el.lotmSf2Sky = sky;
el.classList.add( 'lotm-fog-ready', 'lotm-fog-inline' );
watch( el );
if ( io ) { io.observe( el ); } else if ( !reduceMotion ) { el.classList.add( 'lotm-fog-live' ); }
if ( el.getClientRects().length > 1 ) { multi.push( el ); clip( el ); }
}
function init( root ) {
[].forEach.call( ( root || document ).querySelectorAll( '.spoilerblur' ), fogifyInline );
}
if ( window.mw && mw.hook ) {
mw.hook( 'wikipage.content' ).add( function ( $c ) { $c.each( function () { if ( this.querySelectorAll ) { init( this ); } } ); } );
}
if ( document.readyState === 'loading' ) { document.addEventListener( 'DOMContentLoaded', function () { init(); } ); } else { init(); }
window.addEventListener( 'resize', reclip, { passive: true } );
if ( document.fonts && document.fonts.ready ) { document.fonts.ready.then( reclip ); }
if ( 'ResizeObserver' in window ) {
var main = document.querySelector( '#mw-content-text' ) || document.body, first = true;
new ResizeObserver( function () { if ( first ) { first = false; return; } reclip(); } ).observe( main );
}
// lite включился позже (замер кадров): дрейф снимает CSS, здесь лишь гасим .lotm-fog-live для единообразия с блоками
window.addEventListener( 'lotm-lite', function ( e ) {
if ( e.detail ) { [].forEach.call( document.querySelectorAll( '.lotm-fog-inline.lotm-fog-live' ), function ( el ) { el.classList.remove( 'lotm-fog-live' ); } ); }
} );
}() );
/* ===== TABLES — данные-таблицы статей (article-table / wikitable / fandom-table) =====
Дополнение в конец MediaWiki:Common.js. Пара к TABLES.css.
Что делает (один проход при загрузке, без постоянных наблюдателей кроме scroll/resize):
1) оборачивает таблицу в div.lotm-tw > div.lotm-tw-s (прокрутка внутри обёртки, не страницы);
если Citizen уже обернул .wikitable в .citizen-overflow-wrapper — берёт его обёртку;
2) размечает шапку (tr.lotm-th, 1-й уровень двухуровневой — .lotm-th1),
ячейки первой колонки сетки (.lotm-c0, с учётом rowspan/colspan), группы rowspan
(ячейка .lotm-grp, строка начала группы .lotm-gs), числовые колонки (.lotm-num);
3) классы состояния обёртки: .lotm-tw--wide (таблица шире обёртки → липкая 1-я колонка),
.lotm-tw--l / .lotm-tw--r (есть что листать слева/справа → тени-подсказки).
Не трогает: инфобоксы, навбоксы, .WikiTable (Пути), .lotm-gods-table (Боги), вложенные таблицы. */
( function () {
'use strict';
var SEL = 'table.article-table, table.wikitable, table.fandom-table';
var SKIP = '.portable-infobox, .infobox, .navbox, .lotm-gods-wrap, .lotm-tw, .mw-references-wrap, .va-transcript-text';
var NUM = /^[~≈±+\-−–]?\s?\d[\d\s .,:\/–—\-]*(\s?(д\.\s?п\.|до\s?н\.\s?э\.|г\.|гг\.|год[а-я]*|лет|%|×|x|шт\.?|[a-zа-яё£€$.]{1,9}))?\s?\*?$/i;
var EMPTY = /^(—|–|-|\?|н\/д|нет|)$/i;
function txt( c ) {
return ( c.textContent || '' ).replace( /\[\d+\]/g, '' ).replace( /\s+/g, ' ' ).trim();
}
function eligible( t ) {
if ( t.classList.contains( 'WikiTable' ) || t.classList.contains( 'lotm-gods-table' ) ||
t.classList.contains( 'navbox' ) || t.classList.contains( 'infobox' ) || t.classList.contains( 'lotm-t' ) ) {
return false;
}
if ( t.closest( SKIP ) ) {
return false;
}
// вложенная таблица внутри другой таблицы — оформляет родитель
if ( t.parentElement && t.parentElement.closest( 'table' ) ) {
return false;
}
return t.rows.length >= 2;
}
/* сетка: для каждой ячейки — индекс колонки с учётом rowspan/colspan */
function grid( t ) {
var rows = t.rows, occ = [], out = [], i, j, r, c, col, rs, cs, a, b;
for ( i = 0; i < rows.length; i++ ) {
r = rows[ i ];
occ[ i ] = occ[ i ] || [];
col = 0;
for ( j = 0; j < r.cells.length; j++ ) {
c = r.cells[ j ];
while ( occ[ i ][ col ] ) {
col++;
}
rs = Math.max( 1, c.rowSpan || 1 );
cs = Math.max( 1, c.colSpan || 1 );
for ( a = 0; a < rs && i + a < rows.length; a++ ) {
occ[ i + a ] = occ[ i + a ] || [];
for ( b = 0; b < cs; b++ ) {
occ[ i + a ][ col + b ] = 1;
}
}
out.push( { c: c, row: i, col: col, rs: rs, cs: cs } );
col += cs;
}
}
return out;
}
function mark( t ) {
var rows = t.rows, nHead = 0, i, j, r, allTh, cells, byCol = {}, k, g, n, ok, list, hasGrp = false;
t.classList.add( 'lotm-t' );
cells = grid( t );
var ncol = 0, start = 0, lead = [];
for ( k = 0; k < cells.length; k++ ) {
ncol = Math.max( ncol, cells[ k ].col + cells[ k ].cs );
}
// строки-разделы: одна th на всю ширину («Номиналы», «Банкноты и монеты»)
for ( k = 0; k < cells.length; k++ ) {
g = cells[ k ];
if ( ncol > 1 && g.cs >= ncol && g.c.tagName === 'TH' ) {
g.c.classList.add( 'lotm-sec' );
rows[ g.row ].classList.add( g.row === 0 ? 'lotm-tcap' : 'lotm-secr' );
}
}
if ( rows[ 0 ].classList.contains( 'lotm-tcap' ) ) {
start = 1;
rows[ 0 ].classList.add( 'lotm-th' );
}
// шапка: ведущие строки (после подписи), где все ячейки — th и их больше одной
for ( i = start; i < rows.length && i < start + 3; i++ ) {
r = rows[ i ];
allTh = r.cells.length > 0 && !r.classList.contains( 'lotm-secr' );
for ( j = 0; allTh && j < r.cells.length; j++ ) {
if ( r.cells[ j ].tagName !== 'TH' ) {
allTh = false;
}
}
if ( !allTh ) {
break;
}
lead.push( r );
}
// одиночная строка шапки без тела под ней (таблица-карточка) — не шапка
nHead = start + lead.length;
for ( i = 0; i < lead.length; i++ ) {
lead[ i ].classList.add( 'lotm-th' );
if ( i < lead.length - 1 ) {
lead[ i ].classList.add( 'lotm-th1' );
}
}
if ( lead.length ) {
lead[ lead.length - 1 ].classList.add( 'lotm-thl' );
} else if ( start ) {
rows[ 0 ].classList.add( 'lotm-thl' );
}
for ( k = 0; k < cells.length; k++ ) {
g = cells[ k ];
if ( g.row < nHead ) {
continue;
}
if ( g.col === 0 && g.cs < ncol ) {
g.c.classList.add( 'lotm-c0' );
if ( g.rs > 1 && g.rs < rows.length - nHead + 1 ) {
g.c.classList.add( 'lotm-grp' );
hasGrp = true;
}
}
if ( g.cs === 1 ) {
( byCol[ g.col ] = byCol[ g.col ] || [] ).push( g.c );
}
}
// первая колонка и в шапке (для липкости)
for ( k = 0; k < cells.length; k++ ) {
if ( cells[ k ].row < nHead && cells[ k ].col === 0 && cells[ k ].cs < ncol ) {
cells[ k ].c.classList.add( 'lotm-c0' );
}
}
if ( hasGrp ) {
t.classList.add( 'lotm-t--grp' );
for ( k = 0; k < cells.length; k++ ) {
if ( cells[ k ].row >= nHead && cells[ k ].col === 0 && cells[ k ].cs < ncol ) {
cells[ k ].c.classList.add( 'lotm-grp' );
}
}
for ( i = nHead; i < rows.length; i++ ) {
if ( rows[ i ].cells.length && rows[ i ].cells[ 0 ].classList.contains( 'lotm-c0' ) ) {
rows[ i ].classList.add( 'lotm-gs' );
}
}
}
// числовые колонки: ≥ 60 % непустых ячеек — числа/даты, и таких ≥ 2
for ( k in byCol ) {
list = byCol[ k ];
n = 0;
ok = 0;
for ( j = 0; j < list.length; j++ ) {
var s = txt( list[ j ] );
if ( EMPTY.test( s ) || list[ j ].tagName === 'TH' ) {
continue;
}
n++;
if ( s.length <= 24 && NUM.test( s ) && !list[ j ].querySelector( 'img, ul, ol, table, br' ) ) {
ok++;
}
}
if ( ok >= Math.min( 2, n ) && ok >= 1 && ok >= n * 0.6 && k !== '0' ) {
for ( j = 0; j < list.length; j++ ) {
if ( list[ j ].tagName === 'TD' ) {
list[ j ].classList.add( 'lotm-num' );
}
}
}
}
}
function wrap( t ) {
var p = t.parentElement, w, s;
if ( p && p.classList.contains( 'citizen-overflow-content' ) && p.parentElement ) {
w = p.parentElement;
s = p;
w.classList.add( 'lotm-tw', 'lotm-tw--cz' );
} else {
// Citizen ещё не обернул (его обработчик хука может сработать позже) — запрещаем двойную обёртку
t.classList.add( 'citizen-table-nowrap' );
w = document.createElement( 'div' );
w.className = 'lotm-tw';
s = document.createElement( 'div' );
s.className = 'lotm-tw-s';
p.insertBefore( w, t );
w.appendChild( s );
s.appendChild( t );
}
s.classList.add( 'lotm-tw-s' );
return { w: w, s: s, t: t };
}
function state( o ) {
var s = o.s, sw = s.scrollWidth, cw = s.clientWidth, x = Math.abs( s.scrollLeft ), wide = sw > cw + 1, c0;
o.w.classList.toggle( 'lotm-tw--wide', wide && o.stick );
o.w.classList.toggle( 'lotm-tw--l', wide && x > 1 );
o.w.classList.toggle( 'lotm-tw--r', wide && x + cw < sw - 1 );
if ( wide && o.stick === undefined ) {
// липкая 1-я колонка — только если она не шире ~40 % видимой области
c0 = o.t.querySelector( 'td.lotm-c0, th.lotm-c0' );
o.stick = !!c0 && c0.offsetWidth <= cw * 0.42;
o.w.classList.toggle( 'lotm-tw--wide', o.stick );
}
}
function init( root ) {
var list = ( root || document ).querySelectorAll( '.mw-parser-output ' + SEL.split( ', ' ).join( ', .mw-parser-output ' ) ),
todo = [], i, o, items = [], raf = 0;
for ( i = 0; i < list.length; i++ ) {
if ( eligible( list[ i ] ) ) {
todo.push( list[ i ] );
}
}
if ( !todo.length ) {
return;
}
// чтение: какие таблицы были во всю ширину колонки (width=100% и т. п.)
var full = todo.map( function ( t ) {
var p = t.closest( '.citizen-overflow-wrapper' ) || t, pw = p.parentElement ? p.parentElement.clientWidth : 0;
return pw > 0 && p.offsetWidth >= pw - 4;
} );
// запись
for ( i = 0; i < todo.length; i++ ) {
mark( todo[ i ] );
o = wrap( todo[ i ] );
if ( full[ i ] ) {
o.w.classList.add( 'lotm-tw--full' );
}
items.push( o );
}
function upd() {
raf = 0;
items.forEach( state );
}
function req() {
if ( !raf ) {
raf = requestAnimationFrame( upd );
}
}
items.forEach( function ( it ) {
it.s.addEventListener( 'scroll', function () {
if ( !it.raf ) {
it.raf = requestAnimationFrame( function () {
it.raf = 0;
state( it );
} );
}
}, { passive: true } );
} );
window.addEventListener( 'resize', req, { passive: true } );
// раскрытие свёрнутых таблиц (mw-collapsible) меняет ширину
document.addEventListener( 'click', function ( e ) {
if ( e.target && e.target.closest && e.target.closest( '.mw-collapsible-toggle, .mw-collapsible-text' ) ) {
setTimeout( req, 50 );
}
}, { passive: true } );
req();
}
if ( window.mw && mw.hook ) {
mw.hook( 'wikipage.content' ).add( function ( $c ) {
// Citizen оборачивает .wikitable в своём обработчике того же хука — дождёмся его
setTimeout( function () {
init( $c && $c[ 0 ] );
}, 0 );
} );
} else if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', function () {
init();
} );
} else {
init();
}
}() );
/* TOCFIX2: переход по якорю (пункт «Содержания», ссылка #…) на заголовок внутри неактивной вкладки .wds-tabber -
сначала открываем нужную вкладку (синхронно, до штатной прокрутки к якорю), потом докручиваем к заголовку.
После любого переключения вкладки просим scroll-spy Citizen пересчитать активный пункт (событие scroll).
Плюс: scroll-spy Citizen наблюдает заголовки без id (.mw-heading или h2…h6 с id на вложенном .mw-headline) и сравнивает новый активный набор со старым по el.id -
все id пустые, поэтому набор той же длины считается «не изменился» и подсветка застревает. Даём им уникальное
свойство el.id (lotm-sec-<id заголовка>) без атрибута - селекторы, якоря и TOC это не затрагивает. */
( function () {
function tagHeadings( root ) {
Array.prototype.forEach.call( ( root || document ).querySelectorAll( '.mw-parser-output :is(.mw-heading, h2, h3, h4, h5, h6):not([id])' ), function ( w ) {
var h = w.querySelector( '.mw-headline[id], h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]' );
if ( !h || Object.prototype.hasOwnProperty.call( w, 'id' ) ) { return; }
/* только свойство el.id, без атрибута: Citizen выбирает наблюдаемые заголовки селектором h2:not([id]) */
var key = 'lotm-sec-' + h.id;
Object.defineProperty( w, 'id', {
configurable: true,
get: function () { return this.getAttribute( 'id' ) || key; },
set: function ( v ) { delete this.id; this.setAttribute( 'id', v ); }
} );
} );
}
tagHeadings();
if ( window.mw && mw.hook ) { mw.hook( 'wikipage.content' ).add( function ( $c ) { tagHeadings( $c && $c[ 0 ] ); } ); }
function respy() {
setTimeout( function () { window.dispatchEvent( new Event( 'scroll' ) ); }, 0 );
}
function open( hash, scroll ) {
if ( !hash || hash.length < 2 ) { return false; }
var id;
try { id = decodeURIComponent( hash.slice( 1 ) ); } catch ( e ) { id = hash.slice( 1 ); }
var t = document.getElementById( id );
var panel = t && t.closest( '.wds-tab__content' );
if ( !panel || panel.classList.contains( 'wds-is-current' ) ) { return false; }
/* вложенные вкладки: открываем все неактивные предки по цепочке */
var p = panel.parentElement && panel.parentElement.closest( '.wds-tab__content' );
if ( p && !p.classList.contains( 'wds-is-current' ) ) { open( '#' + ( p.querySelector( '[id]' ) || {} ).id ); }
var tabber = panel.parentElement;
var panels = Array.prototype.filter.call( tabber.children, function ( el ) { return el.classList.contains( 'wds-tab__content' ); } );
var tabs = Array.prototype.filter.call( tabber.querySelectorAll( '.wds-tabs__tab' ), function ( el ) { return el.closest( '.wds-tabber' ) === tabber; } );
var tab = tabs[ panels.indexOf( panel ) ];
if ( !tab ) { return false; }
tab.click();
if ( !panel.classList.contains( 'wds-is-current' ) ) {
/* скрипт вкладок (MediaWiki:Citizen.js) не отреагировал или не загружен - переключаем классы сами */
tabs.forEach( function ( el ) { el.classList.toggle( 'wds-is-current', el === tab ); } );
panels.forEach( function ( el ) { el.classList.toggle( 'wds-is-current', el === panel ); } );
}
if ( scroll ) {
requestAnimationFrame( function () {
var h = t.closest( '.mw-heading, h2, h3, h4, h5, h6' ) || t;
h.scrollIntoView( { block: 'start' } );
} );
}
return true;
}
document.addEventListener( 'click', function ( e ) {
if ( !e.target.closest ) { return; }
var a = e.target.closest( 'a[href^="#"]' );
if ( a ) {
/* синхронно: штатная прокрутка к якорю идёт уже к открытой вкладке; докрутка в rAF - на случай,
если хэш не изменился и браузер не прокрутил */
open( a.getAttribute( 'href' ), true );
}
if ( e.target.closest( '.wds-tabs__tab' ) ) { respy(); }
}, true );
document.addEventListener( 'keydown', function ( e ) {
if ( ( e.key === 'Enter' || e.key === ' ' ) && e.target.closest && e.target.closest( '.wds-tabs__tab' ) ) { respy(); }
}, true );
window.addEventListener( 'hashchange', function () { open( location.hash, true ); } );
if ( location.hash ) { setTimeout( function () { open( location.hash, true ); }, 500 ); }
}() );
/* TOCVERIFY: переход из «Содержания» к заголовку в скрытой вкладке - докрутка TOCFIX2 (scrollIntoView самого h2/h3)
на телефоне ставит заголовок на 73 px ниже, чем штатный переход по якорю (на ПК - на 1 px). Повторяем докрутку
к самому якорю (элемент с id), как это делает браузер: наш rAF зарегистрирован после rAF TOCFIX2 и выполняется после него. */
( function () {
document.addEventListener( 'click', function ( e ) {
var a = e.target.closest && e.target.closest( 'a[href^="#"]' );
if ( !a ) { return; }
var id, href = a.getAttribute( 'href' );
try { id = decodeURIComponent( href.slice( 1 ) ); } catch ( err ) { id = href.slice( 1 ); }
var t = id && document.getElementById( id );
if ( !t || !t.closest( '.wds-tab__content' ) ) { return; }
requestAnimationFrame( function () { t.scrollIntoView( { block: 'start' } ); } );
}, true );
}() );
/* Волна 33 — MERGED33.js (ревизор R1). Добавка поверх /tmp/dep_current/Common.js. */
/* C4-07 (fix/C4.js): у ссылок-перенаправлений на английские названия (Visionary Pathway, Fool Pathway/Abilities…)
браузер показывал английскую всплывающую подсказку (title). ИСХОДНЫЙ fix/C4.js удалял title — это ЛОМАЛО превью ссылок
(Popups выбирает только a[title]; R1: на «Путь Шута»/«Клейн Моретти» превью у перенаправлений пропадало). Здесь title заменяется
русским текстом ссылки. Проверено: превью работают, латинских title на «Путь Шута» 193 → 0 (кроме ссылок без кириллицы в тексте). */
mw.hook( 'wikipage.content' ).add( function ( $c ) {
var root = ( $c && $c[ 0 ] ) || document;
if ( !root.querySelectorAll ) { return; }
[].forEach.call( root.querySelectorAll( 'a.mw-redirect[title]' ), function ( a ) {
var t = a.getAttribute( 'title' );
var txt = ( a.textContent || '' ).replace( /\s+/g, ' ' ).trim();
/* title НЕ удаляем: превью ссылок (Popups) берёт только a[title]; без него превью пропадает (проверено R1).
Подменяем английский title русским текстом ссылки — превью строится по href, ему значение title не важно. */
if ( /[A-Za-z]/.test( t ) && !/[А-Яа-яЁё]/.test( t ) && /[А-Яа-яЁё]/.test( txt ) ) { a.setAttribute( 'title', txt ); }
} );
} );
/* C10-2: пустой абзац <p><br></p> первым/последним в блоке или сразу после заголовка — помечаем, CSS скрывает. */
mw.hook( 'wikipage.content' ).add( function ( $c ) {
var root = ( $c && $c[ 0 ] ) || document;
if ( !root.querySelectorAll ) { return; }
[].forEach.call( root.querySelectorAll( 'p' ), function ( p ) {
if ( p.textContent.trim() !== '' ) { return; }
var k = p.children, i, prev, next;
if ( !k.length ) { return; }
for ( i = 0; i < k.length; i++ ) { if ( k[ i ].tagName !== 'BR' ) { return; } }
prev = p.previousElementSibling; next = p.nextElementSibling;
if ( !prev || !next || /^H[1-6]$/.test( prev.tagName ) || prev.classList.contains( 'mw-heading' ) ) {
p.classList.add( 'lotm-emptyp' );
}
} );
} );