scrapbox-text-bubble-2
機能
scrapboxのリンク先のテキストをその場で吹き出し表示する
リンクをクリックする必要がない
Google MapやYouTubeを表示できる
XSS対策
テキストをコピーできる
このver.では対応しない
2021-05-01 14:33:14 TamperMonkey用codeが全てのprojectsで動くようになってしまっていたのを直した
今まではtext bubbleが2重に出ていてしまっていたようだ
道理で動きがもっさりしていたわけだ
2021-04-15
18:03:03
↓を少し修正
2021-04-14 20:53:51 リンク先ページがなかったら、予め指定した他のprojectに同名のページがないかを探し、あればそれを表示する
既知の問題
bubble内のリンクからカーソルを離したときの処理を作っていない
実装
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.jsimport {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());
}
}