popup-container@0.1.2
子要素を<slot>で挿入するようにしたことで、外部のCSSが適用されるようにした 属性を全て文字列化した
.buttonに対するCSSを全部削った
このComponentの責務じゃない
dependencies
code:script.js
import {html, toChildArray} from '../htm@3.0.4%2Fpreact/script.js';
import {useState, useMemo, useEffect, useCallback, useRef} from '../preact@10.5.13/hooks.js';
import useResizeObserver from '../use-resize-observer@7.0.0/script.js';
import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js';
import register from '../preact-custom-element@4.2.1/script.js';
function PopupContainer(props) {
const {
cursorTop = 0,
cursorLeft = 0,
} = props ?? {};
const open = props?.open === 'true' ? true : false;
const cursorPosition = useMemo(
() => ({styleTop: cursorTop, styleLeft: cursorLeft}),
);
const slotRef = useRef(null);
const { ref, width: buttonContainerWidth = 0 } = useResizeObserver();
const { width: editorWidth = 0 } = useResizeObserver({ ref: scrapboxDOM.editor });
const isEmpty = useMemo(() => slotRef?.current?.assignedNodes?.()?.length === 0, slotRef); const popupMenuStyle = calcPopupMenuStyle(cursorPosition);
const triangleStyle = calcTriangleStyle(cursorPosition, isEmpty);
const buttonContainerStyle = calcButtonContainerStyle(editorWidth, buttonContainerWidth, cursorPosition, isEmpty);
return html`
<style>
.popup-menu {
position:absolute;
left:0;
width:100%;
z-index:300;
transform:translateY(calc(-100% - 14px));
-webkit-user-select:none;
user-select:none;
font-family:"Open Sans",Helvetica,Arial,"Hiragino Sans",sans-serif;
pointer-events:none
}
.popup-menu .button-container {
position:relative;
display:inline-block;
max-width:70vw;
min-width:80px;
text-align:center;
background-color:#111;
padding:0 1px;
border-radius:4px;
pointer-events:auto
}
max-width:90vw
}
max-width:90vw
}
.popup-menu .triangle {
position:absolute;
transform:translateX(-50%);
width:0;
height:0;
border-top:6px solid #111; border-left:8px solid transparent;
border-right:8px solid transparent
}
max-width:80vw;
text-align:left
}
</style>
<div class="popup-menu" style="${popupMenuStyle}" hidden="${!open}">
<div ref="${ref}" class="button-container" style="${buttonContainerStyle}">
<slot ref="${slotRef}"/>
</div>
<div class="triangle" style="${triangleStyle}" />
</div>`;
}
// Custom Elementにする
export const tag = 'x-userscript-popup-container';
座標計算
code:script.js
/** .popup-menu のスタイルを計算する */
const calcPopupMenuStyle = (cursorPosition) => ({ top: cursorPosition.styleTop });
/** .triangle のスタイルを計算する */
const calcTriangleStyle = (cursorPosition, isEmpty) => ({
left: cursorPosition.styleLeft,
...(isEmpty
? {
borderTopColor: '#555',
}
: {}),
});
/** .button-container のスタイルを計算する */
function calcButtonContainerStyle(
editorWidth,
buttonContainerWidth,
cursorPosition,
isEmpty,
) {
const translateX = (cursorPosition.styleLeft / editorWidth) * 100;
// 端に寄り過ぎないように、translateX の上限・下限を設定しておく。
// 値はフィーリングで決めており、何かに裏打ちされたものではないので、変えたかったら適当に変える。
const minTranslateX = (20 / buttonContainerWidth) * 100;
const maxTranslateX = 100 - minTranslateX;
return {
left: cursorPosition.styleLeft,
transform: translateX(-${Math.max(minTranslateX, Math.min(translateX, maxTranslateX))}%),
...(isEmpty
? {
color: '#eee',
fontSize: '11px',
display: 'inline-block',
padding: '0 5px',
cursor: 'not-allowed',
backgroundColor: '#555',
}
: {}),
};
}
test code
code:js
import('/api/code/programming-notes/popup-container@0.1.2/test0.js')
.then(({mount}) => mount());
code:test0.js
import {tag} from './script.js';
import {html, render} from '../htm@3.0.4%2Fpreact/script.js';
import {useState} from '../preact@10.5.13/hooks.js';
import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js';
import {useMutationObserver} from '../useMutationObserver/script.js';
import {throttle} from '../custom-throttle/script.js';
const App = () => {
top: 0,
left: 0,
});
const callback = throttle((mutation) => {
const cursor = mutation.target;
setCursorPosition({
top: +cursor.style.top.slice(0, -2),
left: +cursor.style.left.slice(0, -2),
});
}, 100);
return html<${tag} cursor-top="${cursorPosition.top}" cursor-left="${cursorPosition.left}" open>No item found</${tag}>;
}
export function mount() {
const app = document.createElement('div');
app.dataset.userscriptName= 'popup-test';
document.getElementById('editor').append(app);
render(html<${App} />, app);
}