プレゼンテーションモードでページ番号を表示させるUserScript
プレゼンテーションモードでページ番号を表示させるUserScript
https://gyazo.com/53b8f0ca71e0fd9f499f1039f26e8836
実装
code:script.js
(() => {
'use strict';
const BADGE_ID = 'sbx-slide-number';
// .lines .line:not(.section-N){...} 等から N を抜く。
const STYLE_RULE_RE = /\.line\s*:not\(\.section-(\d+)\)/;
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
const app = () => $('.app');
// ---- Badge ----
const ensureBadge = () => {
let el = document.getElementById(BADGE_ID);
if (!el) {
el = document.createElement('div');
el.id = BADGE_ID;
el.setAttribute('role', 'status');
el.setAttribute('aria-live', 'polite');
// 見た目は必要ならCSSへ。ここは既存どおり最小限のインラインで。
Object.assign(el.style, {
position: 'fixed', right: '24px', bottom: '16px',
padding: '6px 15px', fontSize: '24px', lineHeight: '1',
background: 'rgba(0,0,0,0.5)', color: '#fafafa',
borderRadius: '9999px', zIndex: 2147483647, pointerEvents: 'none', opacity: '0.7'
});
document.body.appendChild(el);
}
return el;
};
// ---- Current from <style> ----
const getCurrentIndexFromStyle = () => {
const styles = $$('style');
for (let i = styles.length - 1; i >= 0; i--) {
const txt = stylesi.textContent || '';
if (txt.indexOf('.section-') === -1) continue;
const m = STYLE_RULE_RE.exec(txt);
if (m) {
const n = Number(m1);
if (Number.isFinite(n)) return n + 1;
}
}
return null;
};
// ---- Total (cached) ----
let cachedTotal = null;
let cachedTitleCount = null;
const recomputeTotal = () => {
const titles = $$('.line.section-title');
const nums = [];
for (const el of titles) {
// classList から section-N を1つ拾う
for (const cls of el.classList) {
const m = /^section-(\d+)$/.exec(cls);
if (m) { nums.push(Number(m1)); break; }
}
}
cachedTitleCount = titles.length;
cachedTotal = nums.length ? Math.max(...nums) + 1 : 0;
};
const getTotal = () => {
const titles = $$('.line.section-title');
if (cachedTotal == null || cachedTitleCount !== titles.length) {
recomputeTotal();
}
return cachedTotal;
};
// ---- Fallback ----
const getCurrentIndexFallback = () => {
const titles = $$('.line.section-title');
const idx = titles.findIndex(el => {
const cs = getComputedStyle(el);
if (cs.display === 'none' || cs.visibility === 'hidden') return false;
const r = el.getBoundingClientRect();
return r.bottom > 0 && r.top < (innerHeight || document.documentElement.clientHeight);
});
return idx >= 0 ? idx + 1 : null;
};
// ---- Render (diff update + rAF throttle) ----
let rafPending = false;
let lastBadgeText = ''; // 差分更新用
const render = () => {
if (rafPending) return;
rafPending = true;
requestAnimationFrame(() => {
rafPending = false;
if (!app()?.classList.contains('presentation')) return;
const badge = ensureBadge();
const total = getTotal();
const cur = getCurrentIndexFromStyle() ?? getCurrentIndexFallback();
const text = (total > 0 && cur != null) ? ${cur} / ${total} : '';
if (text !== lastBadgeText) {
lastBadgeText = text;
badge.textContent = text;
}
});
};
// ---- Lifecycle & Observers ----
let moClass = null;
let moDom = null;
let started = false;
// 変更フィルタ:style 追加/更新 or .lines 配下の変化のみ反応
const shouldUpdate = (mut) => {
if (mut.type === 'characterData') {
return mut.target?.parentNode?.nodeName === 'STYLE';
}
if (mut.type === 'childList') {
const nodes = ...mut.addedNodes, ...mut.removedNodes;
return nodes.some(n => {
if (n.nodeName === 'STYLE') return true;
if (n.nodeType !== 1) return false;
// .lines 自体やその子孫の変化
return n.matches?.('.lines, .lines *') || n.closest?.('.lines');
});
}
return false;
};
const start = () => {
if (started) return;
started = true;
ensureBadge();
recomputeTotal();
render();
moDom = new MutationObserver((muts) => {
if (muts.some(shouldUpdate)) render();
});
moDom.observe(document.body, { subtree: true, childList: true, characterData: true });
};
const stop = () => {
if (!started) return;
started = false;
if (moDom) moDom.disconnect();
const el = document.getElementById(BADGE_ID);
if (el) el.remove();
lastBadgeText = '';
cachedTotal = null;
cachedTitleCount = null;
};
const watchPresentationToggle = () => {
const root = app();
if (!root) return;
const handle = () => (root.classList.contains('presentation') ? start() : stop());
moClass = new MutationObserver(handle);
moClass.observe(root, { attributes: true, attributeFilter: 'class' });
handle();
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', watchPresentationToggle, { once: true });
} else {
watchPresentationToggle();
}
})();
/shinyaoguri/プレゼンテーションモードでページ番号を表示させるUserScript