更新行をスクロールバーに表示するUserScript@0.1.0
originalとの違い
typescriptにした
若干refactoring
機能追加
既読行にも飛べる
code:script.js
var n=u();function r(){n.innerHTML="";for(let e of Array.from(document.getElementsByClassName("telomere-border")))n.appendChild(p(e))}r();var a,s;scrapbox.addListener("lines:changed",()=>{clearTimeout(s),s=setTimeout(()=>{cancelAnimationFrame(a),a=requestAnimationFrame(r)},500)});scrapbox.addListener("layout:changed",r);globalThis.addEventListener("load",({target:e})=>{e instanceof HTMLImageElement&&r()},{capture:!0});n.style.transition="all 0.4s";var l=()=>{n.style.opacity="0.2"},c;globalThis.addEventListener("scroll",()=>{n.style.opacity="1.0",clearTimeout(c),c=setTimeout(l,4*1e3)});scrapbox.on("layout:changed",l);function u(){let e=document.createElement("div");e.style.position="fixed";let o=document.getElementsByTagName("nav")0.getBoundingClientRect().height;return e.style.top=${o}px,e.style.height=calc(100vh - ${o}px),e.style.right="0px",e.style.zIndex="10000",document.body.appendChild(e),e}function p(e){let o=document.documentElement.scrollTop,i=document.body.scrollHeight,{top:d,height:m}=e.getBoundingClientRect(),t=document.createElement("div");return t.style.position="absolute",t.style.backgroundColor=e.classList.contains("unread")?"var(--telomere-unread, #52ba68)":"var(--telomere-border)",t.style.top=${(d+o)/i*100}%,t.style.height=${m/i*100}%,t.style.width="10px",t.style.right="0px",t.style.cursor="pointer",t.addEventListener("click",()=>e.scrollIntoView({block:"center"})),t} 実装したいこと
canvasで描画してみる
現状:行ごとにDOMで作っている
DOMの生成コストが大きい
こうする
細長いcanvasを一つだけ用意する
ページが更新されるのに合わせて、canvasに色を書き込む
canvasをクリックすると、クリック位置に対応する行に飛ぶ
navbarに干渉させないようにしたのは良くなかった
ミニマップとスクロールバーの位置とがずれてしまった 戻そう
2022-03-08
12:38:32 既読部分も押せるようにした
12:15:07 更新処理を間引いた
2022-03-04
11:41:02 mobileで動いていなかったので直した
a.unreadではなくdiv.unreadになるらしい
code readingしつつ書き換えてみる
code:script.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
declare const scrapbox: Scrapbox;
const container = makeViewer();
function render() {
container.innerHTML = ""; // remove all children
for (
const telomere of Array.from(
document.getElementsByClassName("telomere-border"),
)
) {
container.appendChild(makeBar(telomere));
}
}
function ensureUnread(div: unknown): asserts div is HTMLAnchorElement | HTMLDivElement {
if (
div instanceof HTMLAnchorElement ||
div instanceof HTMLDivElement
) return;
throw TypeError("a.unread" must be HTMLAnchorElement | HTMLDivElement but actually ${div});
}
実行
code:script.ts
render();
let animationId: number | undefined;
let timer: number | undefined;
scrapbox.addListener("lines:changed", () => {
clearTimeout(timer);
timer = setTimeout(() => {
cancelAnimationFrame(animationId);
animationId = requestAnimationFrame(render);
}, 500);
});
scrapbox.addListener("layout:changed", render);
code:script.ts
// 画像がロードされた時にlineの高さが変わるので再計算する
globalThis.addEventListener("load", ({ target }) => {
if (!(target instanceof HTMLImageElement)) return;
render();
}, { capture: true });
表示制御
しばらくスクロールしないと見えにくくなる
code:script.ts
// スクロールでフワッと出す
container.style.transition = "all 0.4s";
/** 隠す
*
* 目を凝らせば見えるギリギリの透明度にする
*/
const hide = () => {
container.style.opacity = "0.2";
};
let timerId: number | undefined;
globalThis.addEventListener("scroll", () => {
container.style.opacity = "1.0";
clearTimeout(timerId);
timerId = setTimeout(hide, 4 * 1000); // 読んでいるうちに気づいたら消えている
});
scrapbox.on("layout:changed", hide); // ページリストに飛んだらリセット
DOMの構築
表示領域
code:script.ts
/** ページ全体の更新された行を画面右端に表示 */
function makeViewer(): HTMLDivElement {
const container = document.createElement("div");
container.style.position = "fixed";
const navHeight = document
.getElementsByTagName("nav")0 .getBoundingClientRect().height;
container.style.top = ${navHeight}px;
container.style.height = calc(100vh - ${navHeight}px);
container.style.right = "0px";
container.style.zIndex = "10000";
document.body.appendChild(container);
return container;
}
テロメア
クリックするとその場所に飛ぶようにする
code:script.ts
/** 未読位置に対応した部分を表すやつ
*
* @param telomere 参照する未読テロメア
*/
function makeBar(
telomere: Element
): HTMLDivElement {
const offset = document.documentElement.scrollTop;
const H = document.body.scrollHeight;
const {
top,
height,
} = telomere.getBoundingClientRect();
const bar = document.createElement("div");
bar.style.position = "absolute";
bar.style.backgroundColor =
telomere.classList.contains("unread") ?
"var(--telomere-border)";
bar.style.top = ${(top + offset) / H * 100}%;
bar.style.height = ${height / H * 100}%;
// bar.style.minHeight = "4px"
// TODO: mobileの場合はもっと狭くしてもよさそう
// div.pageの右端と画面右端の間の50%になるように調節するとか?
bar.style.width = "10px";
bar.style.right = "0px";
bar.style.cursor = "pointer";
bar.addEventListener(
"click",
() => telomere.scrollIntoView({ block: "center" }),
);
return bar;
}