script.jsimport {html, toChildArray} from '../htm@3.0.4%2Fpreact/script.js';
import {useState, useMemo, useEffect, useCallback} 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';
export function PopupContainer({
open,
cursorPosition,
children,
}) {
const { ref, width: buttonContainerWidth = 0 } = useResizeObserver();
const { width: editorWidth = 0 } = useResizeObserver({ ref: scrapboxDOM.editor });
const isEmpty = useMemo(() => toChildArray(children).length === 0, [children]);
const popupMenuStyle = calcPopupMenuStyle(cursorPosition);
const triangleStyle = calcTriangleStyle(cursorPosition, isEmpty);
const buttonContainerStyle = calcButtonContainerStyle(editorWidth, buttonContainerWidth, cursorPosition, isEmpty);
return html`
<div class="popup-menu" style="${popupMenuStyle}" hidden="${!open}">
<div ref="${ref}" class="button-container" style="${buttonContainerStyle}">
${children}
</div>
<div class="triangle" style="${triangleStyle}" />
</div>`;
}
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',
}
: {}),
};
}
script.jsexport const CSS = () => 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
}
html[data-os*='android'] .popup-menu .button-container,
.popup-menu .button-container[data-os*='android'] {
max-width:90vw
}
html[data-os*='ios'] .popup-menu .button-container,
.popup-menu .button-container[data-os*='ios'] {
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
}
html[data-os*='android'] .popup-menu.vertical .button-container,
.popup-menu[data-os*='android'].vertical .button-container {
max-width:80vw;
text-align:left
}
.button {
font-size:11px;
color:#eee;
cursor:pointer;
display:inline-block;
padding:0 5px
}
.button:not(:first-of-type) {
border:0;
border-left:1px solid #eee
}
.button.selected {
background-color:#222;
text-decoration:underline
}
html[data-os*='android'] .button {
font-size:13px;
padding:6px;
min-width:12vw
}
html[data-os*='ios'] .button{
font-size:13px;
padding:6px;
min-width:12vw
}
.button div.icon {
height:2em;
max-width:10em;
display:inline-block;
overflow:hidden;
margin-left:1px;
vertical-align:top
}
.button div.icon img {
max-height:100%;
vertical-align:unset
}
html[data-os*='android'] .popup-menu.vertical .button-container .button,
.popup-menu[data-os*='android'].vertical .button-container .button {
font-size:11px;
display:block;
line-height:1.2em;
padding:12px 10px;
min-width:40px;
border-left:0
}
html[data-os*='android'] .popup-menu.vertical .button-container .button:not(:last-of-type),
.popup-menu[data-os*='android'].vertical .button-container .button:not(:last-of-type) {
border:0;
border-bottom:1px solid #eee
}
html[data-os*='ios'] .popup-menu.vertical .button-container,
.popup-menu[data-os*='ios'].vertical .button-container {
max-width:80vw;
text-align:left
}
html[data-os*='ios'] .popup-menu.vertical .button-container .button,
.popup-menu[data-os*='ios'].vertical .button-container .button {
font-size:11px;
display:block;
line-height:1.2em;
padding:12px 10px;
min-width:40px;
border-left:0
}
html[data-os*='ios'] .popup-menu.vertical .button-container .button:not(:last-of-type),
.popup-menu[data-os*='ios'].vertical .button-container .button:not(:last-of-type) {
border:0;
border-bottom:1px solid #eee
}
</style>`;
jsimport('/api/code/programming-notes/popup-container@0.1.0/test0.js');
test0.jsimport {PopupContainer, CSS} from './script.js';
import {html} from '../htm@3.0.4%2Fpreact/script.js';
import {useState} from '../preact@10.5.13/hooks.js';
import register from '../preact-custom-element@4.2.1/script.js';
import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js';
import {useMutationObserver} from '../useMutationObserver/script.js';
import {throttle} from '../custom-throttle/script.js';
const App = () => {
const [cursorPosition, setCursorPosition] = useState({
styleTop: 0,
styleLeft: 0,
});
const callback = throttle((mutation) => {
const cursor = mutation.target;
setCursorPosition({
styleTop: +cursor.style.top.slice(0, -2),
styleLeft: +cursor.style.left.slice(0, -2),
});
}, 100);
useMutationObserver([{current: scrapboxDOM.cursor}], ([mutation]) => callback(mutation), {attributes: true});
return html`<${CSS} /><${PopupContainer} cursorPosition="${cursorPosition}" open>No item found<//>`;
}
register(App, 'popup-test', [], {shadow: true});
document.getElementById('editor').insertAdjacentHTML('afterbegin', '<popup-test></popup-test>');