generated at
scrapbox-preview-box
scrapbox-text-bubble-2で使う本文のpreview window

既知の問題
doneページの読み込みを待てない
かなり致命的な問題
これが原因で表示がおかしくなる
対策
ページの読み込みを待つmethod emitChange を導入する
2021-03-18 01:12:58
done title project の設定それぞれでページの読み込みが始まってしまう
対策
ページの読み込みに setTimeout を使う
100ms程度待てばいいだろう
2021-03-18 05:31:12 やった
title project を設定する特別なmethodを使う
なんだかあれだなtakker
やりたくない
2021-04-13 14:22:28 /project/title として一つの属性にまとめる
これが一番な気がしてきた
これに変えよう
テーマを増やしたい
doneスクロール機能を付けたい
2021-03-18 08:01:01 #ditor をscrollしたいときに面倒かも
08:06:45 スクロールしている間はカーソルが離れていても消えないようにした
doneスクロール機能をつけるとカードの縦横幅が親カードに制限されてしまう
困ったtakker
スクロール機能をつけないほうがいいかな?
ただそうすると、行リンクのhoverをどう表示すればいいかが問題になる
スクロールする
行リンクのhoverが簡単
行リンクのところまでscrollすればいい
入れ子が進むほど、吹き出しが小さくなってしまう
スクロールしない
DOMの入れ子構造をやめれば、本家scrapbox-text-bubbleのように親の吹き出し領域をはみ出して表示できる
行リンクの表示方法を考える必要がある
行リンク以降の文章を表示するとかならどうだろう?takker
これで行ってみるか。
2021-03-19 05:59:28 スクロール機能を削った
2021-04-16 11:02:47 子カードが表示されたら、親カードのスクロールをロックすればいいのでは?

2021-04-16
09:54:24 存在しない行IDは無視する
2021-04-14
20:42:53 get title get project を追加した
20:38:31 空ページを表示しない処理が動いていなかった
20:48:53 再修正
2021-04-13
17:17:17 encodeされたタイトルから正しく行IDを分離できるようにした
14:16:26 作成関数をeasyDOMgeneratorのinterfaceを使って作るようにした
2021-03-18
07:38:57 ↓を修正した
07:19:47 行リンクをhoverすると、該当行まで予めscrollしたscrapbox-text-bubbleを表示する

UI
<scrapbox-preview-box>
属性
project
title
theme
未対応
骨格
html
<div>...</div>
<div> で解析してhtmlにしたテキストを囲むだけ
テキストの表示
CSS
機能
scrapbox-card-bubble用に、mouseover eventを外部に送信する
Shadow DOMを使わなければ、そういう事する必要もないか
Style
script.js
const css = ` :host { padding: 5px 0px 5px 5px; font-size: 11px; line-height: 1.42857; user-select: text; position: absolute; background-color: var(--page-bg, #fefefe); color: var(--page-text-color, #4a4a4a); border-radius: 4px; box-shadow: 0 6px 12px rgba(0,0,0,0.175); /*max-height: 80vh; overflow-y: auto;*/ z-index: 9000; } `;
HTMLとJavascript
script.js
import {parse} from '../scrapbox-preview-box%2Fparser/script.js'; import {fragment, h} from '../easyDOMgenerator/script.js'; customElements.define('preview-box', class extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({mode: 'open', delegatesFocus: true}); shadow.innerHTML = `<style>${css}</style> <div id="box"></div>`; this._box = shadow.getElementById('box'); this._onmouseenterCallback = undefined; this._onmouseleaveCallback = undefined; this._loading = undefined; } connectedCallback() { this.hide(); } get project() { return this._project; } get title() { return this._title; } set path(value) { this.setAttribute('path', value); } set theme(value) { this.setAttribute('theme', value); } position({top, left}) { this.style.top = `${top}px`; this.style.left = `${left}px`; } async show() { await this._loading; // 空っぽの場合は表示しない // defaultで<style>が含まれるので、nodeが2個以上のときのみ表示する if (this._box.childElementCount < 2) { this.hide(); return; } this.hidden = false; if(!this._lineId) return; /* // 行IDまでscrollする // web page全体も一緒にscrollされてしまうので、その分を戻しておく const scrollY = window.scrollY; this.shadowRoot.getElementById(`L${this._lineId}`)?.scrollIntoView?.({block: 'start'}); window.scroll(0, scrollY); */ } hide() { this.hidden = true; } set onmouseenterInLink(callback) { this.shadowRoot.removeEventListener('mouseenter', this._onmouseenterCallback, {capture: true}); this._onmouseenterCallback = e => { if (!e.target.matches('.page-link')) return; callback(e); }; this.shadowRoot.addEventListener('mouseenter', this._onmouseenterCallback, {capture: true}); } static get observedAttributes() { return ['path', 'theme']; } attributeChangedCallback(name, oldValue, newValue) { if (oldValue === newValue) return; switch(name) { case 'path': const [project, title] = decodeURIComponent(newValue) .match(/^\/([\w\-]+)\/(.+)$/).slice(1); this._project = project; this._title = title; this._createBody(); break; case 'theme': // 未実装 break; } } _createBody() { if (!this._project || !this._title) return; this._lineId = this._title.match(/#([\d\w+]+)$/)?.[1]; const title = this._lineId ? this._title.slice(0, -this._lineId.length - 1) : this._title; this._loading = (async () => { const res = await fetch(`/api/pages/${this._project}/${encodeURIComponent(title)}`); if (!res.ok) { this._replace( document.createTextNode('An invalid page source.') ); return; } let {lines} = await res.json(); // title行は除く lines = lines.slice(1); const lineDOMs = parse(lines, this._project, this._title); if (!this._lineId) { this._replace(...lineDOMs); return; } // 行ID以降のみを表示する const id = `L${this._lineId}`; const index = lineDOMs.findIndex(dom => dom.id === id); if (index < 0) { this._replace(...lineDOMs); return; } this._replace(lineDOMs[0], ...lineDOMs.slice(index)); // 先頭の<style>は削っちゃだめ // 行ID部分をhighlightしておく this.shadowRoot.getElementById(id).classList.add('permalink'); })(); } _replace(...newElements) { this._box.textContent = ''; if (newElements.length === 0) return; this._box.append(...newElements); } }); export const previewBox = (...params) => h('preview-box', ...params);

test code
その1
カーソルに追随するwindowを出す
2021-03-01 00:11:50 いい感じ
js
import('/api/code/programming-notes/scrapbox-preview-box/test1.js');
test1.js
import {previewBox as create} from './script.js'; import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js'; (async () => { const box = previewBox({path: '/programming-notes/scrapbox-preview-boxで表示するテキストのDOM構造'}); scrapboxDOM.editor.append(box); const observer = new MutationObserver(async () =>{ box.position({ top: parseInt(scrapboxDOM.cursor.style.top) + 14, left: parseInt(scrapboxDOM.cursor.style.left), }); await box.show(); }); observer.observe(scrapboxDOM.cursor, {attributes: true}); })();
その2
リンクをhoverするとwindowを表示する
2021-03-01 01:26:06 現状の問題点
mouseを外してもcardが消えてくれない
this.hide() previewBox.hide() に直すのを忘れてた
ただこれを直しても消えない
01:59:40 if (timer) return を消したら直った
timer周りを丁寧に処理しないといけなさそう
preview-box内部のリンクに対してpreview-boxを開けない
this ではなく this.shadowRoot に対してevent listenerを登録したらうまくいった
preview-boxの中身の変更にラグがある
2021-03-01 02:03:19 preview boxをネストして表示できるようにする
単に <preview-box> <preview-box> を入れただけでは表示されない
Shadow DOM中に<slot>を入れると表示されるようになる
属性変更と実際にカードの中身が変わるタイミングとにズレがあるのが悪影響を出しているかも
カードの中身が変わった時点で、eventを発行するか?
ネストしまくると、カードが細長くなってしまうのが気になる
親カードの幅からはみ出ないようになるようだ
#editor に全部配置するか?
div#preview-box-list みたいなのを用意して、その中に全て放り込んでおく
直接 #editor に配置してもいいか
javascriptから、 max-width を動的に指定する方法でもいいか
2021-03-01 03:13:09 効果なかった
今の所煩わしいわけではないので、対策は後回しにしよう。
js
import('/api/code/programming-notes/scrapbox-preview-box/test2.js');
test2.js
import {previewBox as create} from './script.js'; import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js'; let timer = null; (async () => { const previewBox = await createPreviewBox(scrapboxDOM.editor); scrapboxDOM.editor.addEventListener('mouseenter', ({target}) => { if (!target.matches('.page-link') || scrapbox.Layout !== 'page') return; onmouseenter(previewBox, {target}); }, {capture: true}); scrapboxDOM.editor.addEventListener('mouseleave', ({target}) => { if (!target.matches('.page-link') || scrapbox.Layout !== 'page') return; fadeoutCallback(previewBox) }, {capture: true}); scrapboxDOM.editor.addEventListener('click', ({target}) => { if (target.matches('preview-box')) return; previewBox.hide(); }, {capture: true}); addEventListener('wheel', () => clearTimeout(timer)); })(); async function createPreviewBox(parentNode) { const previewBox = await create(); let childBox = undefined; previewBox.onmouseenterInLink = async (e) => { if (!childBox) childBox = await createPreviewBox(previewBox); onmouseenter(childBox, e); }; previewBox.addEventListener('mouseenter', () => clearTimeout(timer)); previewBox.addEventListener('mouseleave', () => fadeoutCallback(previewBox)); previewBox.addEventListener('click', ({target}) => { if (target.matches('preview-box')) return; previewBox.hide(); }); parentNode.append(previewBox); return previewBox; } function onmouseenter(previewBox, {target}) { const [_, project, title] = target.href.match(/scrapbox\.io\/([^\/]+)\/(.+)$/); previewBox.path = `/${project}/${title}`; clearTimeout(timer); timer = setTimeout(async () => { const {top, left} = target.getBoundingClientRect(); const parent = previewBox.parentElement.getBoundingClientRect(); previewBox.position({ top: top + 18 - parent.top, left: left - parent.left, }); await previewBox.show(); }, 650); target.addEventListener('mouseleave', () => { clearTimeout(timer); fadeoutCallback(previewBox); }, {once: true}); } function fadeoutCallback(previewBox) { if (previewBox.hidden) return; clearTimeout(timer); timer = setTimeout(() => { previewBox.hide(); }, 650); }

JavaScript

#2021-04-13 14:29:33
#2021-03-18 01:08:23
#2021-03-01 00:43:01
#2021-02-28 21:52:49