generated at
関連ページリストを吹き出し表示するUserScript
next version: ScrapBubble@0.2.0
hr
ScrapScriptsの機能の一つ
UserScriptとして使いたい場合はこれを使う
ScrapScriptsの改造をする前に、こちらのコードをrefactoringしてみるのも手かも
ScrapScriptsの対応するコードと比較するのもよし
名前をつけておこう

使い方
warningまだ開発段階で、大幅にコードを変える可能性があります
使用するときはコードを自分のprojectにコピペするのが無難です
script.js
import {CardBubble} from '/api/code/takker/関連ページリストを吹き出し表示するUserScript/cardBubble.js'; const cardBubble = new CardBubble({ //projects: [...] }); cardBubble.start();
style.css
@import '/api/code/takker/関連ページリストを吹き出し表示するUserScript/cardBubble.css';

既知の問題
doneprojectごとにカードを色分けしたい
a[href^="https://scrapbox.io/takker"] とかで色分けできそう
cssでuserに設定してもらうようにした
done2020-11-11 14:52:34 scroll barがカードに被って見にくい
overflow-x overflow-y #takker-rel-cards-bubble.related-page-list に移動したら直った
done2020-11-11 02:23:19 外部project linkの1 hop linkを吹き出し表示したとき、吹き出しが消えてくれない
多分timerが正しく設定されていない
でもなんでだろう?
あとでconsole.logを使って調べてみる
空リンクで起きていた不具合だった
関連ページリストを吹き出し表示するUserScript#5fa9ec8f1280f00000340ce7で空リンクを除外しているのを直していなかった

やりたいこと
複数のcard-bubbleのリストを表示できるようにする
DOMを考え直す必要がある
DOMをtree上にもたせる
親のstyleに display: none を入れれば、そのtree全てのcard/text-bubbleを消すことができて便利
どうDOMの親子関係を作るかが悩む
表示に影響されないか?
DOMは並べるだけで、javascriptのclassの方でtree構造をもたせる
ScrapScriptsのDOMはこっち
tree構造の管理はしていない

2020-12-14 17:26:41 カードの色を追加した
2020-11-12 19:49:47
mobile版scrapboxでは起動しないようにした
2020-11-12 14:03:24
即座にcardを表示/非表示するoptionを追加した
card以外をclickしたら即座にcardを閉じるようにした
2020-11-11 14:52:15
scroll barの位置を直した
2020-11-10 16:13:13
advanced-related-pagesを使ってcardを作るようにした
空リンクを吹き出し表示の対象にした
自分のprojectでなくても、他のprojectとつながっているかもしれない
2020-11-10 10:38:31
timerの処理を show / hide に入れた
codeがスッキリした
cardをclickしても hide で消さないようにした
だって消す必要ないし
その他コードを少し綺麗にした
2020-10-25 15:08:50 scrapbox-text-bubble内部のリンクも再び対象にした
カードがtext-bubbleの下に隠れないようにz-indexを調節した
それから createPageCard の仕様を変えたので、それに合わせた
2020-10-22 02:33:37 カードがないときに変なwindowが残ってしまう現象をなくせた……はず
試してない
2020-10-13 13:55:44 読み込み直後から吹き出し表示できるようにした
2020-10-13 10:03:51 /shokaiなどのリンクを吹き出し表示の対象から外した
2020-10-12 15:34:31 text-bubble内のリンクを吹き出し表示の対象から外した
大して使わなかったのでtakker
2020-10-09 14:25:32 popup windowのDOMを置く場所を変えた
別のprojectに移ったときにDOMが何故か残って表示されてしまうバグを防ぐため
直っていなかった
displayを制御するしかなさそう
だめだ
同じタブでページ遷移すると、別プロジェクトに移ったとしてもUserScriptが残ってしまう
URLから飛ぶのではなく、scrapbox内のリンクから飛ぶとそうなる
100歩譲ってそれはいいとしても、関連ページリストが崩れて表示されてしまうのがいただけない
位置がおかしい
カードの並び方も複数列になっている
14:47:37 UserCSSだけリセットされるのが問題?
UserScriptのほうでposition: absoluteを指定してみる
14:58:40 直った
やはりUserCSSだけリセットされないのが問題みたい
2020-10-05 17:37:59 preview先のページリンクの関連ページリストを表示できるようにした
実装は簡単だった
#editor から div.page MutationObserverの監視対象を変えただけ
正確な2 hop linkではない
正しくやるには、preview先ページを基準とした2 hop linkを吹き出し表示しないといけない
面倒なので開いているページを基準にしてある
2020-10-04 21:18:36 classを使用して書き換えた


cardBubble.css
#takker-rel-cards-bubble.related-page-list { background-color: #FFF; box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); position: absolute; display: none; padding: 10px; box-sizing: content-box; z-index: 30000; overflow-x: auto; overflow-y: hidden; } .takker-cards { height: 100%; white-space: nowrap; } .takker-cards li{ float: none !important; } /* 余白を詰める */ .takker-cards.grid li.page-list-item a .header { padding-bottom: 0px; } .takker-cards.grid li.page-list-item a .title { float: left; max-height: 40px; -webkit-line-clamp: 2; } .takker-cards.grid li.page-list-item a .description { line-height: normal; padding-top: 0px; } #takker-rel-cards-bubble .page-list-item { display: inline-block; white-space: normal; margin: 0 10px 10px 0; }

projectごとにカードを色分けする設定
headerの色を変えることで区別する
cardBubble.css
.takker-cards { --card-takker-header: rgba(41, 169, 114, 0.5); --card-takker-memex-header: rgba(73, 77, 90, 0.5); --card-takker-private-header: rgba(77, 195, 250, 0.5); --card-daiiz-header: rgba(252, 147, 6, 0.5); } .takker-cards.grid li.page-list-item a[href^="/takker-memex/"] .header { border-top: var(--card-takker-memex-header, --card-title-bg, #f2f2f3) solid 10px; } .takker-cards.grid li.page-list-item a[href^="/takker/"] .header { border-top: var(--card-takker-header, --card-title-bg, #f2f2f3) solid 10px; } .takker-cards.grid li.page-list-item a[href^="/takker-private/"] .header { border-top: var(--card-takker-private-header, --card-title-bg, #f2f2f3) solid 10px; } .takker-cards.grid li.page-list-item a[href^="/daiiz/"] .header { border-top: var(--card-daiiz-header, --card-title-bg, #f2f2f3) solid 10px; }

/daiiz/関連ページを吹き出し表示するを整形して改造したもの
classに書き換えた
原型がだいぶ薄れてきたかも
cardBubble.js
//import {createPageCard} from '/api/code/takker/scrapboxのページカードを作成するscript/script.js'; import {RelatedPageManager} from '/api/code/takker/advanced-related-pages/script.js' import {isMobile} from '/api/code/takker/mobile版scrapboxの判定/script.js'; export class CardBubble { constructor({projects = []}={}) { // mobile版Scrapboxでは起動しない if (isMobile()) { this._enable = false; return; } const bubbleId = 'takker-rel-cards-bubble'; this.target = document.getElementsByClassName('page')[0]; this.editor = document.getElementById('editor'); this._enable = true; this. relatedPageManager = new RelatedPageManager({projects: [...new Set([...projects,scrapbox.Project.name])]}); this.bubble = document.getElementById(bubbleId) ?? (() => { const bubble = document.createElement('div'); bubble.id = bubbleId; bubble.classList.add('related-page-list'); editor.parentNode.appendChild(bubble); bubble.style.backgroundColor = window.getComputedStyle(document.body).getPropertyValue('background-color'); bubble.style.position = 'absolute'; bubble.addEventListener('wheel', e =>{ bubble.scrollBy({left: e.deltaY * 50, behavior: 'smooth'}); e.preventDefault(); }); return bubble; })(); //this.hide(); this.timer = null; }
2020-10-25 14:22:36 Event delegationをnative javascriptのみで実装するコードを使って書き換えた
cardBubble.js
// eventを設定する start() { if (!this._enable) return; // 関連ページリストを計算する this.relatedPageManager.start(); // linkにmouseを置いたときの処理 this.target.addEventListener('mouseenter', async e => { const link = e.target; // project home pageへのリンクを除外する if (!link.matches('.page-link') || /scrapbox\.io\/[\w\-].+\/$/.test(link.href)) return; //this._log('execute popup event.'); this._setBubblePosition(link); const temp = link.href.replace(/https:\/\/scrapbox\.io\/(.*)$/,'$1'); const project = temp.split('/')[0]; const title = decodeURIComponent(temp.split(/\/|#/)[1]); //this._log(`target project: ${project}`); //this._log(`target title: ${title}`); await this.loadCards({ project: scrapbox.Project.name, title: scrapbox.Page.title, link1hop: { project: project, titleLc: title }}); // cardがなければ何もしない if (this.length === 0) return; this.show(); }, {capture: true}); // mouseをlink or cardから放したらcardの表示を取り消すようにする this.target.addEventListener('mouseleave', e => { const link = e.target; // project home pageへのリンクを除外する if (!link.matches('.page-link') || /scrapbox\.io\/[\w\-].+\/$/.test(link.href)) return; this.hide(); }, {capture: true}); this.bubble.addEventListener('mouseleave', () => this.hide()); // card以外をclickしたら直ちにcardを消す this.target.addEventListener('click', e => { const link = e.target; if (link.matches('#takker-rel-cards-bubble.related-page-list')) return; this.hide({immediate: true}); }, {capture: true}); // cardにmouseがあるときはcardを消さない this.bubble.addEventListener('mouseenter', () => { window.clearTimeout(this.timer); }); } // 対象のリンクの直ぐ側に表示するように位置を調節する _setBubblePosition(link) { const rect = link.getBoundingClientRect(); this.bubble.style.height = `120px`; this.bubble.style.maxWidth = `${this.editor.offsetWidth - link.offsetLeft}px`; this.bubble.style.top = `${18 + rect.top + window.pageYOffset - 120 -20 -29}px`; this.bubble.style.left = `${rect.left + window.pageXOffset}px`; } hide({immediate=false}={}) { window.clearTimeout(this.timer); if (immediate) this.bubble.style.display = 'none'; // 少し時間を置いてからcardを消す this.timer = window.setTimeout(() => this.bubble.style.display = 'none', 650); } show({immediate=false}={}) { window.clearTimeout(this.timer); if (immediate) this.bubble.style.display = 'inline-block'; // 少し時間を置いてからcardを表示する this.timer = window.setTimeout(() => this.bubble.style.display = 'inline-block', 650); } get length() { return this.bubble.firstChild.children.length; }

scrapboxのページカードのDOMを作成する
cardBubble.js
async loadCards(linkInfo) { // 一旦カードを非表示にする this.hide({immediate: true}); // 一旦カードをリセット // 二重にカードができてしまう場合があるのでそれも消す while (this.bubble.firstChild){ this.bubble.removeChild(this.bubble.firstChild); } // カードを入れるリストを作成する const cards = document.createElement('div'); cards.classList.add('takker-cards','grid'); //this._log('loading cards...'); // 表示するカードを取得する (await this.relatedPageManager.getCardHtmls(linkInfo)) // cardを作成する .forEach(html =>{ cards.insertAdjacentHTML('beforeend',html); // cardを小さめにする const newCard = cards.lastElementChild; newCard.style.width = '120px'; newCard.style.height = '120px'; }); this.bubble.appendChild(cards); }
cardBubble.js
// debug用 _log(msg, ...objects) { console.log(`[takker-cards]${msg}`, objects); } }


cardBubble.js.disabled
async loadCards(project, title) { // 一旦カードをリセット if (this.bubble.firstChild) { this.bubble.removeChild(this.bubble.firstChild); } // カードを入れるリストを作成する const cards = document.createElement('div'); cards.classList.add('takker-cards','grid'); //console.log('loading cards...'); if (project !== scrapbox.Project.name) { const response = await fetch(`/api/pages/${project}/${title}`); if(!response.ok) return; const pages = await response.json().then(json => json.relatedPages.links1hop); pages.forEach(page => { const newCardText = createPageCard({ project: project, title: page.title, description: page.descriptions.join('\n'), imageUrl: page.image}); cards.insertAdjacentHTML('beforeend',newCardText); // cardを小さめにする const newCard = cards.lastElementChild; newCard.style.width = '120px'; newCard.style.height = '120px'; }); } else { [...document.getElementsByClassName('relation-label')] .filter(label => label.innerText === title) .forEach(label => { let card = label.nextSibling; while (card?.classList?.contains('page-list-item')) { const newCard = card.cloneNode(true); // cardを小さめにする newCard.style.width = '120px'; newCard.style.height = '120px'; cards.appendChild(newCard); card = card.nextSibling; } }); } this.bubble.appendChild(cards); }

#2021-01-04 00:12:36
#2020-11-14 13:53:32
#2020-11-12 03:18:30
#2020-11-11 02:19:59
#2020-11-10 10:41:44
#2020-10-25 14:23:03
#2020-10-22 02:33:27
#2020-10-12 16:12:10
#2020-10-09 14:26:47
#2020-10-05 00:50:31
#2020-10-04 19:24:04
#2020-08-30 02:48:04