関連ページリストを吹き出し表示するUserScript
名前をつけておこう
使い方
まだ開発段階で、大幅にコードを変える可能性があります
使用するときはコードを自分のprojectにコピペするのが無難です
script.jsimport {CardBubble} from '/api/code/takker/関連ページリストを吹き出し表示するUserScript/cardBubble.js';
const cardBubble = new CardBubble({
//projects: [...]
});
cardBubble.start();
style.css@import '/api/code/takker/関連ページリストを吹き出し表示するUserScript/cardBubble.css';
既知の問題
projectごとにカードを色分けしたい
a[href^="https://scrapbox.io/takker"]
とかで色分けできそう
でuserに設定してもらうようにした
2020-11-11 14:52:34
scroll barがカードに被って見にくい overflow-x
と overflow-y
を #takker-rel-cards-bubble.related-page-list
に移動したら直った
多分timerが正しく設定されていない
でもなんでだろう?
あとでconsole.logを使って調べてみる
やりたいこと
複数のcard-bubbleのリストを表示できるようにする
DOMを考え直す必要がある
DOMをtree上にもたせる
親のstyleに display: none
を入れれば、そのtree全てのcard/text-bubbleを消すことができて便利
どうDOMの親子関係を作るかが悩む
表示に影響されないか?
DOMは並べるだけで、
のclassの方でtree構造をもたせる
tree構造の管理はしていない
2020-12-14 17:26:41 カードの色を追加した
2020-11-12 19:49:47
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
自分のprojectでなくても、他のprojectとつながっているかもしれない
2020-11-10 10:38:31
timerの処理を show
/ hide
に入れた
codeがスッキリした
cardをclickしても hide
で消さないようにした
だって消す必要ないし
その他コードを少し綺麗にした
カードが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内のリンクを吹き出し表示の対象から外した
大して使わなかったので
2020-10-09 14:25:32 popup windowのDOMを置く場所を変えた
別のprojectに移ったときにDOMが何故か残って表示されてしまうバグを防ぐため
直っていなかった
displayを制御するしかなさそう
だめだ
URLから飛ぶのではなく、scrapbox内のリンクから飛ぶとそうなる
100歩譲ってそれはいいとしても、関連ページリストが崩れて表示されてしまうのがいただけない
位置がおかしい
カードの並び方も複数列になっている
14:58:40 直った
2020-10-05 17:37:59 preview先のページリンクの関連ページリストを表示できるようにした
実装は簡単だった
正しくやるには、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;
}
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;
}
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;
}
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);
}