scrapbox-preview-box
既知の問題
/icons/done.iconページの読み込みを待てない
かなり致命的な問題
これが原因で表示がおかしくなる
対策
ページの読み込みを待つmethodemitChangeを導入する
2021-03-18 01:12:58
/icons/done.icontitleとprojectの設定それぞれでページの読み込みが始まってしまう
対策
ページの読み込みにsetTimeoutを使う
100ms程度待てばいいだろう
2021-03-18 05:31:12 やった
titleとprojectを設定する特別なmethodを使う
なんだかあれだなtakker.icon
やりたくない
2021-04-13 14:22:28 /project/titleとして一つの属性にまとめる
これが一番な気がしてきた
これに変えよう
テーマを増やしたい
/icons/done.iconスクロール機能を付けたい
2021-03-18 08:01:01 #ditorをscrollしたいときに面倒かも
08:06:45 スクロールしている間はカーソルが離れていても消えないようにした
/icons/done.iconスクロール機能をつけるとカードの縦横幅が親カードに制限されてしまう
困ったtakker.icon
スクロール機能をつけないほうがいいかな?
ただそうすると、行リンクのhoverをどう表示すればいいかが問題になる
スクロールする
行リンクのhoverが簡単
行リンクのところまでscrollすればいい
入れ子が進むほど、吹き出しが小さくなってしまう
スクロールしない
行リンクの表示方法を考える必要がある
行リンク以降の文章を表示するとかならどうだろう?takker.icon
これで行ってみるか。
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を分離できるようにした
2021-03-18
07:38:57 ↓を修正した
07:19:47 行リンクをhoverすると、該当行まで予めscrollしたscrapbox-text-bubbleを表示する
UI
<scrapbox-preview-box>
属性
project
title
theme
未対応
骨格
code:html
<div>...</div>
<div>で解析してhtmlにしたテキストを囲むだけ
テキストの表示
CSS
機能
Shadow DOMを使わなければ、そういう事する必要もないか
Style
code:script.js
const css = `
:host {
padding: 5px 0px 5px 5px;
font-size: 11px;
line-height: 1.42857;
user-select: text;
position: absolute;
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
code: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() {
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
switch(name) {
case 'path':
.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(lineDOMs0, ...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 いい感じ
https://gyazo.com/cc375485c83ef7e026b4ace6b86b257f
code:js
import('/api/code/programming-notes/scrapbox-preview-box/test1.js');
code: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
https://gyazo.com/fd0294171f38f7269c159f6053cc4f97
リンクを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 効果なかった
今の所煩わしいわけではないので、対策は後回しにしよう。
code:js
import('/api/code/programming-notes/scrapbox-preview-box/test2.js');
code: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.icon