ScrapBubble@0.2.0
/icons/hr.icon
お試し
code:js
(async () => {
const {mount} = await import('/api/code/programming-notes/ScrapBubble@0.2.0/script.js');
})();
code:tamperMonkey.js
// ==UserScript==
// @name ScrapBubble
// @version 0.2.1
// @description n hop先のテキストを表示する
// @author takker
// @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();
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');
window.onload = undefined;
});
実装したいこと
カードを並び替える
updatedやproject nameを使って並び替える
/icons/done.icontitle行にカーソルを置くと逆リンクを表示する
Stream pageでもbubbleできるようにする?
そこまでしなくてもなー……takker.icon
/icons/done.icon/xxx形式のリンクは除外する もしくは、そのprojectのpinどめしたページを表示する機能をつける?
これはこれで面白そうかも
/icons/doing.iconprojectに応じてthemeを変える
後もうちょい
card bubbleからのbubbleを無効にするoption
whiteListに、透過的に扱いたいprojectのリストを入れる感じですかねyosider.icon
そうそうtakker.icon
内部リンクにも有効にするにはuserscriptを発動させるproject自身も入れる必要がある?yosider.icon
${scrapbox.Project.name}を入れておけば、複数プロジェクトで共通の設定で使えますかね
リストの重複を除去しておいたほうが良いか
そういえば重複除去を実装してなかったtakker.icon
でも重複してても問題はないのか
読み込みが遅くなったりしない?
card bubbleが重複しちゃうか。
単に重複を除去するだけならSetを使えばいいので簡単だけど、takker.iconは順番を維持して重複を除去したい どうやればいいかな
ならSet使っても問題ないか
userscriptを発動させるprojectは、自動でbubble対象に追加されますtakker.icon
試しにwhiteListを空にしてみるとわかります
そうだったのかyosider.icon
閉じるボタンをつける
行リンクをhoverしたら、該当行までscrollする
/icons/ほしい.iconblu3mo.icontakker.icon*3
実は以前のversionではあった
色々書き換えているうちに闇に葬り去られた
そうなのかblu3mo.icon
被リンクを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
https://gyazo.com/573f220cde2db9eb11d67452514be83a
色ついてるのわかりやすい!yosider.iconblu3mo.icon
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を消す
https://gyazo.com/ae5d793a73bb04a6c40fe0a68f4ba1e7
https://gyazo.com/98407354e5c49853881b50f257591f09
色々おかしい
/icons/done.iconCSSが変
なんで縦スクロールが出てくるの?
13:19:54 scriptが死んでて表示されない……
/icons/done.icon逆リンク元のページが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 とりあえずこんな感じ
https://gyazo.com/bae185132c1f437b1deb77f6b2bdcd20
カードからマウスを離しても勝手に消えないようにした
カードの外をクリックすると消える
scroll lockまで実装した
Indexed DBとかにわざわざ格納する必要ないや
fetchしたら一定時間cacheをとってこない設定を入れたい
fetchの数を減らす
2021-06-10
23:46:11 親カードをスクロールできちゃうのは気持ち悪い
https://gyazo.com/522384ea98bdd9dab1a8fc428f13b807
子カードがいるときはスクロールできないようにする
.no-scrollを付けて、overflow-y: hidden;にする
23:53:36 うーん、CSSを変更したときに一瞬ガクつくなあ
https://gyazo.com/6227181acf96b09cb94e035fb46a2038
しゃーない。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 /icons/done.iconいちいちUpdating...が表示されるのが気に障る
https://gyazo.com/16cfaeda8abba3b2679b6e2d5d0349a2
対策
cacheの読み込みと表示とを別の函数にする
pointerenterを検知した時点で、読み込みを開始する
setTimeoutが発火したら表示する
この時cacheの読み込みはしない
23:11:36 done
https://gyazo.com/e21db4af7458c52f85daffc7127a133b
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
code: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 {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') ?
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);
});
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}], if (target.classList.contains("enter")) return;
hide(0);
},
{attributes: true, attributeFilter: "class"} );
return html`
<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);
}