ScrapBubble@0.2.0
お試し
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を使って並び替える
title行にカーソルを置くと逆リンクを表示する
Stream pageでもbubbleできるようにする?
そこまでしなくてもなー……
もしくは、そのprojectのpinどめしたページを表示する機能をつける?
これはこれで面白そうかも
projectに応じてthemeを変える
後もうちょい
card bubbleからのbubbleを無効にするoption
whiteList
に、透過的に扱いたいprojectのリストを入れる感じですかね
そうそう
内部リンクにも有効にするにはuserscriptを発動させるproject自身も入れる必要がある?
${scrapbox.Project.name}
を入れておけば、複数プロジェクトで共通の設定で使えますかね
リストの重複を除去しておいたほうが良いか
そういえば重複除去を実装してなかった
でも重複してても問題はないのか
読み込みが遅くなったりしない?
card bubbleが重複しちゃうか。
単に重複を除去するだけならSetを使えばいいので簡単だけど、は順番を維持して重複を除去したい どうやればいいかな
ならSet使っても問題ないか
userscriptを発動させるprojectは、自動でbubble対象に追加されます
試しに whiteList
を空にしてみるとわかります
そうだったのか
閉じるボタンをつける
行リンクをhoverしたら、該当行までscrollする
実は以前のversionではあった
色々書き換えているうちに闇に葬り去られた
そうなのか
被リンクをhoverすると、該当行までscrollする
リンクが最初に現れる箇所を探し、そこまでscrollさせればいい
ハッシュタグからhoverした場合はscrollしない
GitHubに移す
既知の問題
project themeの読み込みにラグがある
マウスカーソルを動かしていると突然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:11:52 span.page-link
に対してbubbleを出す処理を呼び出していた
エラーが出まくりなので調査中
12:50:57 .page-link
から取り出した encodedTitleLc
を自前でdecodeしておく
正規表現で行IDを入れないようにした
多分うまく行った
00:50:46 ↓revert
今度は行IDまでタイトルに含まれてしまった
きちんと文字列処理をやったほうがいい
2021-06-11
23:11:05 themeを生やした
selectorをいじった
15:41:27 とりあえずリリース
まだ色々問題は残っている
styleの調節
色とかカスタマイズできるようにしたい
配色が悪いところを調節したい
外部projectに対しても節操なくbubbleしちゃう
追々直す
13:57:47 card bubbleの隙間をクリックすると子bubbleを消す
色々おかしい
CSSが変
なんで縦スクロールが出てくるの?
13:19:54 scriptが死んでて表示されない……
逆リンク元のページがcard bubbleに表示されちゃっている
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まで実装した
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
いちいち
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 ↓動いた
.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.jsimport {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);
}