generated at
scrapbox-text-bubble-2
UserScriptのみをもちいたscrapbox-text-bubble

機能
scrapboxのリンク先のテキストをその場で吹き出し表示する
リンクをクリックする必要がない
ScrapScriptsにはない機能
Google MapやYouTubeを表示できる
XSS対策
ScrapScriptsはhtmlのコードブロックを実際のDOMとして間違えて読み込んでしまうというバグがある
テキストをコピーできる
予め指定した外部プロジェクトのリンク先テキストも吹き出し表示できる
吹き出し表示したページの中にあるリンクでscrapbox-card-bubbleが使える
このver.では対応しない

2021-05-01 14:33:14 TamperMonkey用codeが全てのprojectsで動くようになってしまっていたのを直した
今まではtext bubbleが2重に出ていてしまっていたようだ
道理で動きがもっさりしていたわけだ
2021-04-15
18:03:03
リンクとscrapbox-preview-boxがかぶっていたのを修正した
↓を少し修正
17:52:37 試験的に/takker/関連ページリストを吹き出し表示するUserScriptからtext-bubbleを表示できるようにした
2021-04-14 20:53:51 リンク先ページがなかったら、予め指定した他のprojectに同名のページがないかを探し、あればそれを表示する

既知の問題
bubble内のリンクからカーソルを離したときの処理を作っていない
scrapbox-preview-boxで実装し忘れている部分
scrapbox-preview-boxでの実装を書いてから、scrapbox-text-bubble-2でも実装する

実装

js
(async () => { const {TextBubble} = await import('/api/code/programming-notes/scrapbox-text-bubble-2/script.js'); new TextBubble(); })();

TamperMonkey用
js
// ==UserScript== // @name scrapbox-text-bubble-2 // @namespace https://scrapbox.io // @version 0.2 // @description リンク先テキストを表示する // @author takker // @match https://scrapbox.io/* // @license MIT // @copyright Copyright (c) 2021 takker // ==/UserScript== "use strict" window.onload = async () => { // 自分が所属しているprojectは除く const res = await fetch('/api/projects'); const {projects} = await res.json(); // scrapbox objectがまだ出来ていない可能性があるので、URLからproject nameを取得する if (projects.map(({name}) => name).includes(location.pathname.match(/^\/([\w\-]+)/)[1])) return; const {TextBubble} = await import('/api/code/programming-notes/scrapbox-text-bubble-2/script.js'); new TextBubble(); };

script.js
import {previewBox} from '../scrapbox-preview-box/script.js'; import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js'; export class TextBubble { constructor({projects = [], delay = 650} = {}) { this._list = []; this._timer = null; this._acceptProjects = [...new Set([...projects, scrapbox.Project.name])]; this._delay = delay; // rootのevent設定 scrapboxDOM.editor.parentElement.addEventListener('pointerenter', ({target}) => { const link = target.closest('.page-link, .page-list-item > a'); if (!link) return; this._onenterLink(0, link); }, {capture: true}); // リンクから離れたら scrapboxDOM.editor.parentElement.addEventListener('pointerleave', ({target}) => { if (!target.matches('.page-link, .page-list-item > a')) return; this._onleave(0); }, {capture: true}); scrapboxDOM.editor.parentElement.addEventListener('click', ({target}) => { if (target.matches('preview-box')) { const index = this._indexOf(target); this._hide(index + 1); return; } this._hide(0); }, {capture: true}); // 次ページに遷移するときには全部消す const observer = new MutationObserver(() => { clearTimeout(this._timer); this._remove(0); }); observer.observe(document.getElementsByTagName('title')[0], {childList: true}); } _indexOf(target) { return this._list.indexOf(target); } _onenterBox(index) { this._onleave(index + 1); this._show(index); } _onenterLink(index, target) { const {top, left} = target.getBoundingClientRect(); const root = scrapboxDOM.editor.getBoundingClientRect(); const path = target.href.slice('https://scrapbox.io'.length); if (!this._acceptProjects.includes(path.match(/^\/([\w\-]+)/)[1])) return; this._add(index, { path, // targetのselectorによって変える top: top + (target.matches('.page-link') ? 18 : 140) - root.top, left: left - root.left, }); clearTimeout(this._timer); this._timer = setTimeout(() => this._show(index), this._delay); } _onleave(index) { clearTimeout(this._timer); this._timer = setTimeout(() => this._hide(index), this._delay); } // indexまで表示する async _show(index) { if (index < 0 || this._list.length <= index) return; await Promise.all(this._list.slice(0, index + 1).map(async box => { await box.show(); if (!box.hidden) return; // リンク先テキストが存在しなかった場合は、別のprojectから探してくる for (const project of this._acceptProjects) { box.path = `/${project}/${box.title}`; await box.show(); if (!box.hidden) return; } })); } // index以下を非表示にする _hide(index) { if (index < 0 || this._list.length <= index) return; this._list.slice(index).forEach(box => box.hide()); } // 特定の位置に追加する _add(index, {path, top, left}) { if (index < 0 || this._list.length < index) return; // 新しく追加するので、最大要素番号 + 1まで許容する // 挿入地点のboxとpathが等しい場合は何もしない if (path === this._list[index]?.path) return; const box = previewBox({path}); box.position({top, left}) // eventの設定 box.onmouseenterInLink = ({target}) => this._onenterLink(index + 1, target); box.addEventListener('pointerenter', () => { console.log(`[scrapbox-text-bubble-2]Enter No. ${index}`); this._onenterBox(index); }, {capture: true}); box.addEventListener('pointerleave', ({screenX, screenY}) => { // 他のpreview-boxにいた場合は無視 if (document.elementFromPoint(screenX, screenY).matches('preview-box')) return; console.log(`[scrapbox-text-bubble-2]Leave No. ${index}`); this._onleave(index); }, {capture: true}); this._remove(index); this._list.push(box); scrapboxDOM.editor.append(box); } // index以下を全て消す _remove(index) { if (index < 0 || this._list.length <= index) return; const deleteList = this._list.slice(index); this._list = this._list.slice(0, index); deleteList.forEach(box => box.remove()); } }

#2021-04-13 14:14:41
#2021-03-19 06:08:31