text-bubble-component
変えるところ
component内でfetchしない
テキストデータを外部から属性経由で提供させる
属性変化の挙動
lines: 表示するテキスト
新しいテキストを構文解析してDOMに反映させる
タイトル行は予め除いて渡す
path: /:project/:title
DOMの<a>のパスを変更する
構文解析はしない
アイデア
空リンクの識別ができたら良いなーと思ったtakker.icon
hoverして空リンクだったら、空リンクの色に変えるようにすると便利そう
空リンクかどうかは、何らかの手段で伝える
textBubble.changeToEmptyLink({project: 'xxx', title: 'yyy'})とか?
2021-05-01 16:42:10 なぜかCSSの色が適用されない?
.empty-page-linkではなく:hostの色がついてしまっている
classはちゃんと当てられている
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);
z-index: 9000;
}
max-height: 80vh;
overflow-y: auto;
}
`;
import {parse} from '../scrapbox-preview-box%2Fparser/script.js';
import {fragment, h} from '../easyDOMgenerator/script.js';
const TAG_NAME = 'text-bubble';
customElements.define(TAG_NAME, 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._eventCallbacks = {};
}
connectedCallback() {
this.hide();
}
get project() {
return this._project;
}
get title() {
return this._title;
}
set path(value) {
this.setAttribute('path', value);
}
set contents(value) {
this.setAttribute('contents', value);
}
set theme(value) {
this.setAttribute('theme', value);
}
position({top, left}) {
this.style.top = ${top}px;
this.style.left = ${left}px;
}
show() {
// 空っぽの場合は表示しない
// 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;
}
changeToEmptyLink(project, title) {
this._box.querySelectorAll(.page-link[href^="/${project}/${title}"])
.forEach(a => a.classList.add('empty-page-link'));
}
on(event, selecter, callback) {
if (this._eventCallbacksevent) this.shadowRoot.removeEventListener(event, this._eventCallbacksevent, {capture: true}); this._eventCallbacksevent = e => { if (!e.target.matches(selecter)) return;
callback(e);
};
this.shadowRoot.addEventListener(event, this._eventCallbacksevent, {capture: true}); }
static get observedAttributes() {
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
switch(name) {
case 'path':
.match(/^\/(\w\-+)\/(.+)$/).slice(1); this._changeLinkPath(this._project, this._title, project, title);
this._project = project;
this._title = title;
break;
case 'lines':
this._createBody(JSON.parse(newValue));
break;
case 'theme':
// 未実装
break;
}
}
_createBody(lines) {
const lineDOMs = parse(lines.slice(1), this._project, this._title);
if (!this._lineId) {
this._replace(...lineDOMs);
return;
}
// 行IDがある場合は、行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');
}
_changeLinkPath(oldProject, oldTitle, newProject, newTitle) {
this._box.querySelectorAll(.page-link[href^="/${oldProject}"])
.forEach(a => {
const pathname = (new URL(a.href)).pathname;
a.href = /${newProject}${pathname.slice(/${oldProject}.length)};
});
this._box.querySelectorAll(.code-block-start a[href^="/api/code/${oldProject}"]).forEach(a => {
const pathname = (new URL(a.href)).pathname;
a.href = /api/code/${newProject}/${newTitle}${pathname.slice(/api/code/${oldProject}/${oldTitle}.length)};
});
this._box.querySelectorAll(.table-block-start a[href^="/api/table/${oldProject}"]).forEach(a => {
const pathname = (new URL(a.href)).pathname;
a.href = /api/table/${newProject}/${newTitle}${pathname.slice(/api/table/${oldProject}/${oldTitle}.length)};
});
}
_replace(...newElements) {
this._box.textContent = '';
if (newElements.length === 0) return;
this._box.append(...newElements);
}
});
export const textBubble = (...params) => h(TAG_NAME, ...params);
test code
code:js
import('/api/code/programming-notes/text-bubble-component/test1.js');
code:test1.js
import {textBubble} from './script.js';
import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js';
(async () => {
const path = '/programming-notes/scrapbox-preview-boxで表示するテキストのDOM構造';
const res = await fetch(/api/pages${path});
const {lines} = await res.json();
const box = textBubble({path, lines: JSON.stringify(lines)});
scrapboxDOM.editor.append(box);
const observer = new MutationObserver(async () =>{
box.position({
top: parseInt(scrapboxDOM.cursor.style.top) + parseInt(scrapboxDOM.cursor.style.height),
left: parseInt(scrapboxDOM.cursor.style.left),
});
box.show();
});
observer.observe(scrapboxDOM.cursor, {attributes: true});
})();