card-bubble-component@0.1.0
https://gyazo.com/64f61a2c292241bd772ec57bbaa5ddc6
既知の問題
/icons/done.icon画像が歪んでいる
https://gyazo.com/5333a47fc5267a381b3df14d94798c17
DOMを次のようにscrapboxと同じ構造に直したら直った
code:diff
+<div class="thumbnail"><img src="${thumbnail}"/></div>
-<img class="thumbnail" src="${thumbnail}"/>
原因
widthとheightとで別々に長さを決めていた
2021-06-17
16:20:26 同一projectへのカードだったときに、SPAを維持したページ繊維ができていなかった code:diff
+ target="${project !== scrapbox.Project.name ? '_blank' : ''}">
- target="${project !== scrapbox.Project.name && '_blank'}">
後者だと同一projectのときにtarget="false"となってしまう
target=""にしないとダメみたい
2021-06-13
2021-06-12
19:16:09 typo: s/blank=/target=/
16:47:19 リンク記法にできてた影を消した
16:41:13
projectごとに色を変えるようにした
文字を小さめにした
paddingも縮めた
setter経由でobjectを受け渡すようにすれば、任意のobjectを受け取るpropsを生やす事ができる
16:27:34 やめた
書くのが非常にめんどくさすぎる
15:24:49 Shadow DOMにしようとしたが断念した
謎の余白ができてしまう
styleを読んでも原因がわからなかった
https://gyazo.com/dea523deb89fb0a90a8cb0a5fecdd3ed
2021-06-11
23:13:19 theme属性を生やした
08:14:00 実装終了
数式記法以外はテストできたと思う
まだ動かない
dependencies
code:script.js
import {html} from '../htm@3.0.4%2Fpreact/script.js';
import {useState, useMemo, useCallback} from '../preact@10.5.13/hooks.js';
import {useKaTeX} from '../use-KaTeX/script.js';
import {parse} from '../scrapbox-parser@7.1.0/script.js';
import {toLc} from '../scrapbox-titleLc/script.js';
export const RelatedPageCard = ({
project, title, descriptions, thumbnail, theme,
onPointerEnterCapture,
onPointerLeaveCapture,
}) => {
const blocks = useMemo(() => thumbnail ? [] :
parse(descriptions.join('\n'), {hasTitle: false}),
);
return html`
<a class="related-page-card page-link" type="link"
data-theme="${theme}"
onPointerEnterCapture="${onPointerEnterCapture}"
onPointerLeaveCapture="${onPointerLeaveCapture}"
href="/${project}/${toLc(title)}"
rel="${project === scrapbox.Project.name ? 'route' : 'noopner noreferrer'}"
target="${project !== scrapbox.Project.name ? '_blank' : ''}">
<div class="hover" />
<div class="content">
<div class="header">
<div class="title">${title}</div>
</div>
${thumbnail ?
html<div class="thumbnail"><img src="${thumbnail}"/></div> :
html`<div class="description">
${blocks.flatMap((block, index) => block.type === 'line' ?
[html`<p key="${index}">
${block.nodes.map(node =>
html<${Node} node="${node}" project="${project}" />
)}
</>`] : []
)}
</div>`
}
</div>
</a>
`;
};
DOMの組み立て
code:script.js
const Node = ({node, project}) => {
switch(node.type) {
case 'code':
return html<code>${node.text}</code>;
case 'formula':
return html<${Formula} node="${node}" />;
case 'commmandLine':
return html<code>${node.symbol} ${node.text}</code>;
case 'helpfeel':
return html<code>? ${node.text}</code>;
case 'quote':
case 'strong':
case 'decoration':
return node.nodes.map(node =>html<${Node} node="${node}" project="${project}" />);
case 'googleMap':
case 'image':
case 'strongImage':
return html;
case 'icon':
case 'strongIcon':
return html<${Icon} node="${node}" project="${project}" />;
case 'hashTag':
return html<${HashTag} node="${node}" project="${project}" />;
case 'link':
return html<${Link} node="${node}" project="${project}" />;
case 'plain':
case 'blank':
return html${node.text};
}
};
const Formula = ({node: {formula}}) => {
const {ref, error, setFormula} = useKaTeX('');
setFormula(formula);
return html`
<span class="formula${error ? ' error' : ''}">
${!error && html<span class="katex-display" ref="${ref}"/>}
</span>`;
}
const Icon = ({node: {pathType, path}, project: _project}) => {
const project = pathType === 'relative' ? _project : path.match(/\/(\w\-+)/)1; const title = pathType === 'relative' ? path: path.match(/\/\w\-+\/(.+)$/)1; return html<img class="icon" src="/api/pages/${project}/${toLc(title)}/icon" />;
};
const HashTag = ({node: {href}, project}) => html<span class="page-link">#${href}</span>;
const Link = ({node: {pathType, href, content}, project}) => pathType !== 'absolute' ?
html<span class="page-link">${href}</span> :
// contentが空のときはundefinedではなく''になるので、
// ??ではなく||でfallback処理をする必要がある
html<span class="link">${content || href}</span>;
css
projectのthemeに応じて枠線の色などを変える
code:script.js
export const CSS = `
--card-title-bg: hsl(0, 0%, 39%);
}
--card-title-bg: hsl(0, 0%, 89%);
}
--card-title-bg: hsl(53, 8%, 58%);
}
--card-title-bg: hsl(203, 42%, 17%);
}
--card-title-bg: hsl(227, 68%, 62%);
}
--card-title-bg: hsl(267, 39%, 60%);
}
--card-title-bg: hsl(136, 29%, 50%);
}
--card-title-bg: hsl(43, 71%, 51%);
}
--card-title-bg: hsl(4, 58%, 56%);
}
--card-title-bg: hsl(72, 64%, 57%);
}
--card-title-bg: hsl(331, 21%, 26%);
}
--card-title-bg: hsl(176, 29%, 67%);
}
code:script.js
.related-page-card {
display: block;
position: relative;
height: inherit;
width: inherit;
overflow: hidden;
text-overflow: ellipsis;
font-family: "Roboto",Helvetica,Arial,"Hiragino Sans",sans-serif;
background-color: var(--card-bg, #fff); color: var(--card-title-color, #555); word-break: break-word;
text-decoration: none;
}
.related-page-card:hover {
box-shadow: var(--card-box-hover-shadow, 0 2px 0 rgba(0,0,0,0.23));
}
.related-page-card:focus {
outline: 0;
box-shadow: 0 0px 0px 3px rgba(102,175,233,0.6);
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s
}
.related-page-card.hover {
opacity: 0;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: var(--card-hover-bg, rgba(0,0,0,0.05));
mix-blend-mode: multiply;
z-index: 1;
transition: background-color .1s
}
.related-page-card:hover .hover{
opacity: 1;
}
.related-page-card:active .hover{
opacity: 1;
background-color: var(--card-active-bg, rgba(0,0,0,0.1))
}
.related-page-card .content {
height: calc(100% - 5px);
width: inherit;
display: flex;
flex-direction: column;
overflow: hidden;
}
.related-page-card .content .header {
width: 100%;
text-overflow: ellipsis;
border-top: var(--card-title-bg, #f2f2f3) solid 10px; padding: 8px 10px;
}
.related-page-card .content .header .title {
font-size: 11px; /* 14 * 0.8 */
line-height: 16px; /* 20 * 0.8 */
font-weight: bold;
max-height: 48px; /* 60 * 0.8 */
margin: 0;
overflow: hidden;
display: block;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
}
.related-page-card .content .description {
line-height: 16px; /* 20 * 0.8 */
padding: 8px 10px 0;
font-size: 10px; /* 12 * 0.8 */
white-space: pre-line;
column-count: 1;
column-gap: 2em;
column-width: 10em;
height: inherit;
color: var(--card-description-color, gray);
flex-shrink: 16;
overflow: hidden;
}
.related-page-card .content .thumbnail {
display: block;
width: 100%;
margin: 0 auto;
padding: 0 5px;
}
.related-page-card .content .description p {
margin: 0;
display: block;
}
.related-page-card .content .description code {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
font-size: 90%;
background-color: var(--code-bg, rgba(0,0,0,0.04));
padding: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.related-page-card .content .description .icon {
height: 9px; /* 11 * 0.8 */
vertical-align: middle;
}
.related-page-card .content .description .page-link {
background-color: transparent;
text-decoration: none;
cursor: pointer;
}
`;
code:js
import('/api/code/programming-notes/card-bubble-component@0.1.0/test1.js')
.then(({execute}) => execute());
code:test1.js
import {RelatedPageCard, CSS} from './script.js';
import {html, render} from '../htm@3.0.4%2Fpreact/script.js';
import {useState, useEffect} from '../preact@10.5.13/hooks.js';
import {useMutationObserver} from '../useMutationObserver/script.js';
const toLc = title => title.toLowerCase().replaceAll(' ', '_').replaceAll('/', '%2F');
const App = () => {
const path, setPath = useState({project: scrapbox.Project.name, title: scrapbox.Page.title}); useMutationObserver(
[{current: document.getElementsByClassName("page-wrapper")0}], if (target.classList.contains("enter")) return;
setPath({project: scrapbox.Project.name, title: scrapbox.Page.title});
},
{attributes: true, attributeFilter: "class"} );
useEffect(() => (async () => {
const res = await fetch(/api/pages/${path.project}/${toLc(path.title)});
const {relatedPages: {links1hop}} = await res.json();
setPages(links1hop.map(page => ({project: path.project, ...page})));
return html`
<style>
${CSS}
</style>
<ul style="position: fixed; top: 5vh; left: 5vw; list-style: none;">
${pages.map(({project, title, descriptions, image}) => html`
<${RelatedPageCard}
project="${project}" title="${title}"
descriptions="${descriptions}" thumbnail="${image}" />
`)}
</ul>
`;
};
export async function execute() {
const app = document.createElement('div');
app.dataset.userscriptName= 'related-card-list-test';
document.getElementById('editor').append(app);
app.attachShadow({mode: 'open'});
render(html<${App} />, app.shadowRoot);
}