generated at
wheel操作で見出しにジャンプするUserScript

お試し
js
import('/api/code/programming-notes/wheel操作で見出しにジャンプするUserScript/script.js');

実装したいこと
開閉ボタンをつける
position: fixed で、brand iconのすぐ下に置きたい

2021-07-16
08:40:30 use-lines-change@0.1.0を使った
2021-07-12
12:33:57 ちょっとだけrefactoring
2021-07-07
17:05:12 Page Menuから開閉できるようにした
UIが自然じゃない
ボタンとリストが反対側にある
遠すぎ
画面が小さいときリストが表示されなくなる
16:44:07 これは仕方のないことなのだが、切り替えるたびにweb browser履歴に残ってしまう
たくさんwheelをコロコロすると大量の履歴が発生する
16:43:24 wheelで切り替えられるようにした
16:17:44 とりあえずできた
選択すると太字になるようにした
欄が小さすぎるのが欠点
scrapboxの編集画面の左端にcomponentを表示するのは止めたほうがいいかも
それとも/customize/関連リンクを左側に表示するを使って幅を広げる?
.expandable-menu みたく開閉できるようにするか?


dependencies
script.js
import {html, render} from '../htm@3.0.4%2Fpreact/script.js'; import {useState, useEffect, useCallback} from '../preact@10.5.13/hooks.js'; import {useToggle} from '../use-toggle@0.1.0/script.js'; import {useLinesChange} from '../use-lines-change@0.1.0/script.js'; const App = () => { const [{id: selectedId, sections}, update] = useSections(); // ページが編集されるたびにリストを更新する useLinesChange(update, {initialize: true}); // wheel操作で見出しを切り替える const handleWheel = useCallback(e => { const index = sections.findIndex(({id}) => id === selectedId); const {deltaY} = e; if (index <= 0 && deltaY < 0) return; if (index === sections.length - 1 && deltaY > 0) return; e.preventDefault(); e.stopPropagation(); if (deltaY > 0) { sections[index + 1].jump(); return; } if (deltaY < 0) { sections[index - 1].jump(); return; } }, [sections, selectedId]); // Page Menuで開閉する const [open, toggleOpen] = useToggle(false); useEffect(() => scrapbox.PageMenu.addMenu({ title: 'wheel見出し', image: 'https://gyazo.com/bc38721e0980f2188f1c831754ac8da4/raw', onClick: () => toggleOpen(), }), []); return html` <style> :host { position: sticky; top: 45px; z-index: 1001; } ul { margin-right: 10px; border-radius: 3px; min-width: 160px; max-width: 50vw; overflow-x: hidden; overflow-y: auto; padding: 5px 0; margin: 2px 0 0; list-style: none; font-size: 14px; background-color: var(--page-bg, #fefefe); border: 1px solid var(--dropdown-border-color, rgba(0,0,0,0.15)); border-radius: 4px; box-shadow: 0 6px 12px var(--dropdown-shadow-color, rgba(0,0,0,0.175)); background-clip: padding-box; white-space: nowrap; text-align: left; } a { display: flex; clear: both; align-items: center; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; text-decoration: none; color: var(--page-text-color, #4a4a4a); overflow: hidden; text-overflow: ellipsis; width: 100%; } a.selected { font-weight: bold; outline: 0; } a:hover { background-color: var(--card-hover-bg, #f5f5f5); } </style> ${open && html`<ul onWheel="${handleWheel}"> ${sections.map(({id, text, jump}) => id === selectedId ? html`<li><a class="selected" href="#${id}" onClick="${jump}">${text}</a></li>` : html`<li><a href="#${id}" onClick="${jump}">${text}</a></li>` )} </ul>`} ` }

見出しを作るcustom hook
script.js
function useSections() { const [sections, setSections] = useState([]); const [selected, setSelected] = useState(''); const jump = useCallback(id => { setSelected(id); location.hash = id; }, []); const update = useCallback(() => setSections( scrapbox.Page.lines.flatMap( ({section: {start}, id, text}) => start ? [{ id, text, jump: () => jump(id), }] : [] ) ), [jump]); return [{id: selected, sections}, update]; }

script.js
const app = document.createElement('div'); app.attachShadow({mode: 'open'}); app.dataset.userscriptName = 'section-jumper'; document.querySelector('.col-page-side')?.append?.(app); render(html`<${App} />`, app.shadowRoot);