generated at
popup-container@0.1.0
popup-menu@0.1.0からメニュー選択機能などを削ったもの

dependencies
script.js
import {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', } : {}), }; }

css
script.js
export 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>`;


test code
js
import('/api/code/programming-notes/popup-container@0.1.0/test0.js');
test0.js
import {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>');