fix(tracker): тройной хук — bubble, capture, monkey-patch setParaTab

Юзер докладывает, что клик по пилюле не вызывает body click handler
(никаких логов после клика). Возможные причины: capture-listener
расширения браузера со stopPropagation, CSS overlay, что-то ещё.

Чтобы гарантированно ловить клики ВНЕ зависимости от bubble-цепочки:
1) Bubble click на body (как было)
2) Capture click на document (фаза до bubble)
3) Monkey-patch window.setParaTab — функцию, которую chemistry-9 и
   physics-9 зовут inline через onclick. Перехват на уровне JS-функции
   работает даже если event-стек сломан.

Защита от двойного срабатывания: pill.__tbVisited флаг на 100мс.

Если setParaTab определяется позже tracker'а — короткий poll 20*100мс.
This commit is contained in:
Maxim Dolgolyov
2026-05-27 17:44:29 +03:00
parent 5e49fd5835
commit 1e1c0e95f7
+54 -14
View File
@@ -225,24 +225,64 @@
не добавит дубликат. Эта «избыточность» лечит самовосстановлением
ситуации, когда localState.read был засорён старым syncPending-багом
(есть ключ локально, нет на сервере, mark_read иначе никогда не уйдёт). */
function recordParaVisit(key) {
if (!key) return;
localState.last = key;
if (!localState.read.includes(key)) {
localState.read.push(key);
}
refreshPillUI(key);
refreshCheckUI(key);
persist();
syncToServer({ mark_read: key });
}
function wirePillTracking() {
// Hook 1: всплытие click до body. Работает в обычном HTML.
document.body.addEventListener('click', e => {
const tag = e.target && e.target.tagName;
const cls = e.target && e.target.className;
console.log('[tracker] click event target:', tag, '| class:', cls, '| has .para-pill[data-para] up the tree:', !!e.target.closest('.para-pill[data-para]'));
const pill = e.target.closest('.para-pill[data-para]');
const pill = e.target && e.target.closest && e.target.closest('.para-pill[data-para]');
if (!pill) return;
const key = pill.dataset.para;
console.log('[tracker] клик по пилюле', key);
localState.last = key;
if (!localState.read.includes(key)) {
localState.read.push(key);
}
refreshPillUI(key);
refreshCheckUI(key);
persist();
syncToServer({ mark_read: key });
console.log('[tracker] клик по пилюле (bubble)', pill.dataset.para);
recordParaVisit(pill.dataset.para);
});
// Hook 2: capture-фаза (ловит до того, как кто-то остановит propagation).
document.addEventListener('click', e => {
const pill = e.target && e.target.closest && e.target.closest('.para-pill[data-para]');
if (!pill) return;
// Защита от двойного срабатывания — отметим pill сразу.
if (pill.__tbVisited) return;
pill.__tbVisited = true;
setTimeout(() => { pill.__tbVisited = false; }, 100);
console.log('[tracker] клик по пилюле (capture)', pill.dataset.para);
recordParaVisit(pill.dataset.para);
}, true);
// Hook 3: monkey-patch setParaTab — кликом по пилюле химия/физика 9 вызывают
// inline onclick="setParaTab('pN')". Перехват напрямую = работает даже если
// event-bubbling сломан расширением браузера или CSS overlay.
function patchSetParaTab() {
if (typeof window.setParaTab !== 'function' || window.setParaTab.__tbPatched) return;
const orig = window.setParaTab;
const wrapped = function (para) {
try {
if (para && /^p\d+$/i.test(String(para))) {
console.log('[tracker] setParaTab перехвачен', para);
recordParaVisit(String(para));
}
} catch (e) { console.warn('[tracker] patch error:', e); }
return orig.apply(this, arguments);
};
wrapped.__tbPatched = true;
window.setParaTab = wrapped;
console.log('[tracker] setParaTab успешно обёрнут');
}
patchSetParaTab();
// Если страница определяет setParaTab позже — поймаем через короткие опросы.
let tries = 0;
const ivl = setInterval(() => {
patchSetParaTab();
if (typeof window.setParaTab === 'function' && window.setParaTab.__tbPatched) clearInterval(ivl);
if (++tries > 20) clearInterval(ivl);
}, 100);
}
/* ── 8. Inject styling for read-pills (subtle green dot) ─────── */