generated at
ScrapBubble@0.2.0
hr
ScrapBubblepreactで書き換えたver.

お試し
js
(async () => { const {mount} = await import('/api/code/programming-notes/ScrapBubble@0.2.0/script.js'); mount({whiteList: ['takker', 'yosider']}); })();
tamperMonkey.js
// ==UserScript== // @name ScrapBubble // @namespace https://scrapbox.io // @version 0.2.1 // @description n hop先のテキストを表示する // @author takker // @downloadURL https://scrapbox.io/api/code/programming-notes/ScrapBubble/tampermonkey.js // @match https://scrapbox.io/* // @exclude https://scrapbox.io/settings // @license MIT // @copyright Copyright (c) 2021 takker // ==/UserScript== "use strict"; window.addEventListener('load', async () => { // 自分が所属しているprojectは除く const res = await fetch('/api/projects'); const {projects} = await res.json(); // #editorが読み込まれるまで待機 // cf. https://scrapbox.io/scrapbox-drinkup/puppeteerでscrapboxを自動化してイテレーション資料を宣言的に完成させる_(2019%2F9%2F5) await new Promise(resolve => { let timer = null; timer = setInterval(() => { if (!document.getElementById('editor')) return; clearInterval(timer); resolve(); }, 1000); }); // 念の為1秒くらい待っとく await new Promise((resolve) => setTimeout(() => resolve(), 1000)); if (projects.map(({name}) => name).includes(scrapbox.Project.name)) { console.info(`You belong to "/${scrapbox.Project.name}".`); return; } console.info(`You don't belong to "/${scrapbox.Project.name}".`); const {mount} = await import('/api/code/programming-notes/ScrapBubble@0.2.0/script.js'); mount({whiteList: ['takker', 'yosider']}); window.onload = undefined; });

実装したいこと
カードを並び替える
updated やproject nameを使って並び替える
donetitle行にカーソルを置くと逆リンクを表示する
ScrapBubbleにあった機能
Stream pageでもbubbleできるようにする?
そこまでしなくてもなー……takker
done/xxx形式のリンクは除外する
もしくは、そのprojectのpinどめしたページを表示する機能をつける?
これはこれで面白そうかも
doingprojectに応じてthemeを変える
後もうちょい
card bubbleからのbubbleを無効にするoption
ScrapScriptsと同じ仕様にする
ScrapBubble@0.2.0/use-cardsに、予め与えられたproject listからもfetchしてくる機能をつける
whiteList に、透過的に扱いたいprojectのリストを入れる感じですかねyosider
そうそうtakker
内部リンクにも有効にするにはuserscriptを発動させるproject自身も入れる必要がある?yosider
${scrapbox.Project.name} を入れておけば、複数プロジェクトで共通の設定で使えますかね
リストの重複を除去しておいたほうが良いか
そういえば重複除去を実装してなかったtakker
でも重複してても問題はないのか
読み込みが遅くなったりしない?
ScrapBubble@0.2.0/use-cards#60c29eda1280f0000003e1efの代入処理が重複して走るだけなので、遅くはならないはずです
card bubbleが重複しちゃうか。
単に重複を除去するだけならSetを使えばいいので簡単だけど、takkerは順番を維持して重複を除去したい
どうやればいいかな
Setは順番を保持するっぽい
ならSet使っても問題ないか
userscriptを発動させるprojectは、自動でbubble対象に追加されますtakker
試しに whiteList を空にしてみるとわかります
そうだったのかyosider
閉じるボタンをつける
行リンクをhoverしたら、該当行までscrollする
ほしいblu3motakkertakkertakker
実は以前のversionではあった
色々書き換えているうちに闇に葬り去られた
そうなのかblu3mo
被リンクをhoverすると、該当行までscrollする
リンクが最初に現れる箇所を探し、そこまでscrollさせればいい
ハッシュタグからhoverした場合はscrollしない
ハッシュタグは/nishio/容器のメタファーで用いたい
GitHubに移す

既知の問題
project themeの読み込みにラグがある
マウスカーソルを動かしていると突然themeが反映される。
use-project-themeが原因か?
どのlogicが原因?
debuggerで変数を追跡しないとわからなさそう

2021-06-29
00:34:58 expired の単位は秒だった。ミリ秒じゃない
2021-06-13
22:51:26 #editor 外を押してもページが消えるようにした
↓で document から #editor click の範囲を縮めていたのを戻した
うまく消えないときがあって困っていた
scrapboxの端っことか
ページ下部の関連ページリストとかもそう
たくさんbubbleしてよく関連ページリストの上にまでbubbleが来てしまうことがある
それらのbubbleを一気に消すときに、いちいち #editor までスクロールしてからクリックしないと行けない
面倒
pointerenter などのeventをlistenする範囲を #editor に絞った
document に対して発火させると、 .matches() がないとエラーが出たり、(おそらく) scrapbox.Layout === 'page' 以外で動作したりしてしまう
2021-06-12
18:31:41 たぶんScrapBubble@0.2.0#60c329331280f00000ee8337を実装できたtadatadatada
色ついてるのわかりやすい!yosiderblu3mo
18:11:52 span.page-link に対してbubbleを出す処理を呼び出していた
18:11:01 ScrapBubble@0.2.0#60c329331280f00000ee8337を実装している
エラーが出まくりなので調査中
12:50:57 .page-link から取り出した encodedTitleLc を自前でdecodeしておく
01:06:36 scrapbox-titleLcでタイトルをencodeした
正規表現で行IDを入れないようにした
多分うまく行った
00:50:46 ↓revert
今度は行IDまでタイトルに含まれてしまった
きちんと文字列処理をやったほうがいい
00:45:18 URL.pathnameを使うと。タイトル末尾に ? # があった場合、それらを削ってしまう
2021-06-11
23:11:05 themeを生やした
use-project-themeを使った
selectorをいじった
ScrapBubble@0.2.0/use-cards#60c32e481280f00000ee834aをやって不必要にtext bubbleが表示されないようにした
15:41:27 とりあえずリリース
まだ色々問題は残っている
styleの調節
react-wcCSS in JSしたい
18:14:48 status-bar@0.1.0はできた
色とかカスタマイズできるようにしたい
配色が悪いところを調節したい
外部projectに対しても節操なくbubbleしちゃう
追々直す
13:57:47 card bubbleの隙間をクリックすると子bubbleを消す
11:19:00 scrapbox-card-bubbleを表示できた
色々おかしい
doneCSSが変
なんで縦スクロールが出てくるの?
ScrapBubbleのCSSを参考に作り直す
13:19:54 scriptが死んでて表示されない……
done逆リンク元のページがcard bubbleに表示されちゃっている


11:04:36 ScrapBubble@0.2.0/CardBubbleをrenderに追加したらDOMException: String contains an invalid characterが発生したので原因を調べてる
新しく追加したScrapBubble@0.2.0/CardBubbleがバグっているのは確か
11:07:25 念の為React.Fragmentがバグに関係ないことを確認した
11:10:35 解決した
属性エラーだった
単に ...rest ...${rest} としていなかっただけだった
08:17:25 componentとhooksを別ページに切り出す
08:27:12 終了
これ以降、作業ログが各ページに分散する
01:04:22 ページ遷移時にカードをすべて消す
00:45:27 空ページの場合は <TextBubble> 自体を表示しない
00:32:18 とりあえずこんな感じ
カードからマウスを離しても勝手に消えないようにした
カードの外をクリックすると消える
scroll lockまで実装した
あとはscrapbox-card-bubbleを実装するだけ
データはapi/code/:projectname/:pagetitleからとってくる
外部projectを透過的に扱うときも、このAPIを複数project分叩けばいいんじゃね?
Indexed DBとかにわざわざ格納する必要ないや
fetchしたら一定時間cacheをとってこない設定を入れたい
fetchの数を減らす
2021-06-10
23:46:11 親カードをスクロールできちゃうのは気持ち悪い
子カードがいるときはスクロールできないようにする
.no-scroll を付けて、 overflow-y: hidden; にする
23:53:36 うーん、CSSを変更したときに一瞬ガクつくなあ
しゃーない。JSの方でscrollを相殺しよう。
2021-06-11 00:31:02 直した
Componentに切り出した時の構文ミスを直すのに時間食った
xxx="${yyy}" = の前後にスペースが入るとpropertyとして認識されなくなる
/> > にしてた
etc.
23:14:05 入れ子にtext bubbleを出せるようにする
listenerは .text-bubble に付与する
要素番号を深さにする
useCards を複数のtext bubbleに対応させる
show() hide() に深さ depth を渡す
一つの深さには一つのcardしか表示できないようにする
ある深さのtext bubbleが差し替えられたら、それ以降のtext bubbleをすべて消す
23:37:18 実装した
試す
23:44:55 いい感じ
23:44:58 cardを消す処理も実装した
23:46:05 いい感じ
23:04:24 doneいちいち Updating... が表示されるのが気に障る
対策
cacheの読み込みと表示とを別の函数にする
pointerenter を検知した時点で、読み込みを開始する
setTimeout が発火したら表示する
この時cacheの読み込みはしない
23:11:36 done
22:55:51 カードは表示されるが、読み込み中表示がfetch後も残ってしまう
objectの変更を検知できてないみたい
別のeventが発生すると変更が反映される?
これの仕組みはよくわからない
23:02:20 objectをまるごと再生成したら解決した
20:46:29 欲しい物
ページデータを保持する配列
利用側はproject nameとtitleでデータを取得する
取得時にはまずcacheを返す
同時に新しいデータをfetchして更新する
updated を生やしておく
利用側はhookの性質を利用して自動的に更新させる
読み込まれているかどうかを示す loading をすべてのページデータに生やしておく
利用側の想定
lines の中身をparseして表示する
空なら空っぽのまま表示する
loading true なら loading... と表示する
updated が変化したら更新する
依存配列に updated だけ入れておく
ガワはこっちで作る
読み込み中表示とかを .text-bubble に入れたい
20:42:26 ↓動いた
19:38:15 とりあえず試しにtext-bubble-component@0.2.0を深さ1だけ表示できるようにしてみる
.page-link に対して pointerenter で500ms後に表示する
.page-link に対して pointerleave でtimerを消す
表示されていたら何もしない
card以外をクリックしたら消す
表示するカードは cards で管理する
project : 表示するカードのproject
title : 表示するカードのtitle
titleLc :
visible : 表示しているかどうか
lines : 表示するデータの中身
カード外クリックは、 target div[data-userscript-name="text-bubble"] かどうかで判断できる
shadow DOMで隠蔽してあるので、中の要素のクリックは↑になる

dependencies
script.js
import {TextBubble, CSS as textCSS} from '../ScrapBubble@0.2.0%2FTextBubble/script.js'; import {CardBubble, CSS as listCSS} from '../ScrapBubble@0.2.0%2FCardBubble/script.js'; import {RelatedPageCard, CSS as cardCSS} from '../card-bubble-component@0.1.0/script.js'; import {html, Fragment, render} from '../htm@3.0.4%2Fpreact/script.js'; import {useState, useCallback} from '../preact@10.5.13/hooks.js'; import {useCards} from '../ScrapBubble@0.2.0%2Fuse-cards/script.js'; import {useEventListener} from '../use-event-listener/script.js'; import {useProjectTheme} from '../use-project-theme/script.js'; import {useMutationObserver} from '../useMutationObserver/script.js'; import {toLc} from '../scrapbox-titleLc/script.js'; const userscriptName = 'scrap-bubble'; export const App = ({delay = 500, expired = 60, whiteList = []} = {}) => { const {cards, cache, show, hide} = useCards({expired, whiteList}); const [timer, setTimer] = useState(null); const {getTheme, loadTheme} = useProjectTheme(); const showCard = useCallback((depth, link) => { if (!link.matches('a.page-link, .line-title .text')) return; const [_, project, encodedTitleLc] = link.classList.contains('page-link') ? link.href.match(/\/([\w\-]+)\/([^#]*)/) ?? ['','',''] : ['', scrapbox.Project.name, scrapbox.Page.title]; if (project === '') return; const titleLc = toLc(decodeURIComponent(encodedTitleLc)); loadTheme(project); cache(project, titleLc); setTimer(before => { clearTimeout(before); return setTimeout(() => { const {top, right, left, bottom} = link.getBoundingClientRect(); const root = document.getElementById('editor').getBoundingClientRect(); const adjustRight = (left - root.left)/ root.width > 0.5; // 右寄せにするかどうか show(depth, project, titleLc, { top: Math.round(bottom - root.top), bottom: Math.round(root.bottom - top), ...(adjustRight ? {right: Math.round(root.right - right)} : {left: Math.round(left - root.left)} ), }); }, delay); }); }, [cache, show, loadTheme]); const cancel = useCallback(({target}) => { if (!target.matches('a.page-link, .line-title .text')) return; setTimer(before => { clearTimeout(before); return null; }); }, []); const editor = document.getElementById('editor'); useEventListener(editor, 'pointerenter', ({target}) => showCard(0, target), {capture: true}); useEventListener(editor, 'pointerleave', cancel, {capture: true}); useEventListener(document, 'click', ({target}) => { if (target.dataset.userscriptName === userscriptName) return; hide(0); }, {capture: true}); useMutationObserver( [{current: document.getElementsByClassName("page-wrapper")[0]}], ([{target}]) => { if (target.classList.contains("enter")) return; hide(0); }, {attributes: true, attributeFilter: ["class"]} ); return html` <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.12.0/katex.min.css" /> <style> * {box-sizing: border-box;} ${textCSS} ${listCSS} ${cardCSS} </style> ${cards.map(({ project, titleLc, lines, position: {top, left, bottom, right}, linked, loading, }, index) => html`<${Fragment} key="/${project}/${titleLc}/"> <${TextBubble} project="${project}" titleLc="${titleLc}" theme="${getTheme(project)}" style="top: ${top}px; ${left ? `left: ${left}` : `right: ${right}`}px;" lines="${lines}" loading="${loading}" onPointerEnterCapture="${({target}) => showCard(index + 1, target)}" onPointerLeaveCapture="${cancel}" onClick="${() => hide(index + 1)}" hasChild="${cards.length > index + 1}" /> <${CardBubble} loading="${loading}" style="bottom: ${bottom}px; ${left ? `left: ${left}` : `right: ${right}`}px;" onClickCapture="${({target}) => target.tagName !== 'A'&& hide(index + 1)}" hasChild="${cards.length > index + 1}"> ${linked.map(page => html` <${RelatedPageCard} key="/${page.project}/${page.title}" project="${page.project}" title="${page.title}" theme="${getTheme(page.project)}" descriptions="${page.descriptions}" thumbnail="${page.image}" onPointerEnterCapture="${({target}) => showCard(index + 1, target)}" onPointerLeaveCapture="${cancel}" /> `)} <//> <//>`)} `; }; export function mount({delay = 500, expired = 60, whiteList = []} = {}) { const app = document.createElement('div'); app.dataset.userscriptName= userscriptName; document.getElementById('editor').append(app); app.attachShadow({mode: 'open'}); render(html`<${App} delay="${delay}" expired="${expired}" whiteList="${whiteList}"/>`, app.shadowRoot); }