dropdown-container@0.1.1
2021-06-27
Shadow DOMにしたので、こっちのstyleに入れないと適用されない
messagesが配列でないDOMのときに対応した
messagesがundefined | falseのときに対応した
複数の属性を設定できるようにした
messagesに渡したComponentを一つづつ<li>で囲む
01:24:59 custom elementにchildrenを渡す方法を試行錯誤している
とりあえずこれでうまくいくことはわかった
code:js
const component = ({children}) => html<ul>${children.props.children}</ul>;
register(component, 'x-component', [], {shadow: true});
const Component = props => html<x-component ...${props} />;
Shadow DOMの外にも同じchildrenが生成されてしまうのが欠点
childrenを別のproperty nameに変更すれば、余計なDOMが生成されずに済む
code:js
const component = ({_children}) => html<ul>${_children}</ul>;
register(component, 'x-component', [], {shadow: true});
const Component = ({children, ...props}) => html<x-component _children="${children}" ...${props} />;
code:script.js
import {html} from "../htm@3.0.4%2Fpreact/script.js";
import register from '../preact-custom-element@4.2.1/script.js';
import {useRef, useEffect} from '../preact@10.5.13/hooks.js';
const DropdownMenu = ({props}) => {
const ref = useRef(null);
const {children, messages, position, selected} = props;
// 要素が描画領域外にあったら、scrollして見えるようにする
useEffect(() => {
const ul = ref.current;
const selectedLi = ul?.querySelector?.(li[data-key="${selected}"]);
if (!selectedLi) return;
const root = ul.getBoundingClientRect();
const item = selectedLi.getBoundingClientRect();
if (root.top > item.top) {
ul.scrollTop -= root.top - item.top;
return;
}
if (root.bottom < item.bottom) {
ul.scrollTop += item.bottom - root.bottom;
return;
}
// browserの表示領域内にも収める
const {top, bottom} = selectedLi.getBoundingClientRect();
if (top < 0 ) {
selectedLi.scrollIntoView();
return;
}
if (bottom > window.innerHeight) {
selectedLi.scrollIntoView(false);
return;
}
return html`
<style>
ul {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
min-width: 160px;
max-width: 50vw;
max-height: 80vh;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px;
font-weight: normal;
line-height: 28px;
text-align: left;
color: var(--dropdown-text-color, #333); background-color: var(--dropdown-bg, #fff); border: 1px solid var(--dropdown-border-color, rgba(0,0,0,0.15));
border-radius: 4px;
box-shadow: 0 6px 12px var(--dropdown-shadow-color, rgba(0,0,0,0.175));
background-clip: padding-box;
white-space: nowrap;
flex-direction: column;
list-style: none;
overflow-x: hidden;
overflow-y: auto;
}
li > * {
display: flex;
padding: 0px 20px;
clear: both;
align-items: center;
}
li > a {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
text-decoration: none;
color: inherit;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
li > a:hover {
color: var(--dropdown-item-hover-text-color, #262626); background-color: var(--dropdown-item-hover-bg, #f5f5f5); }
li.selected {
color: var(--dropdown-item-hover-text-color, #262626); background-color: var(--dropdown-item-hover-bg, #f5f5f5); outline: 0;
box-shadow: 0 0px 0px 3px var(--dropdown-item-select-border-color, #66afe999); transition: border-color ease-in-out 0.15s,box-shadow ease-in-out 0.15s;
}
li.message {
pointer-events: none;
cursor: not-allowed;
}
img {
height: 1.3em;
margin-right: 3px;
display: inline-block;
}
</style>
<ul ref="${ref}" style="top: ${position.top}px; left: ${position.left}px;">
${(Array.isArray(messages) ? messages : messages ? messages : []).map(message => html<li class="message">${message}</li>)} ${children.map(
child => html`
<li key="${child.key}"
data-key="${child.key}"
class="${child.key === selected && 'selected'}"
${child}
</li>
`)}
</ul>`;
};
// Custom Elementにする
const tag = 'x-userscript-dropdown-menu';
register(DropdownMenu, tag, 'props', {shadow: true}); const dropdownMenu = props => html<${tag} props="${props}" />;
export {dropdownMenu as DropdownMenu};
test code
https://gyazo.com/a2b7d66c0d1dd9f80bf450146134bcdd
code:js
import('/api/code/programming-notes/dropdown-container@0.1.1/test.js')
.then(({mount}) => mount());
code:test.js
import {DropdownMenu} from './script.js';
import {html, render} from '../htm@3.0.4%2Fpreact/script.js';
import {useState, useMemo} from '../preact@10.5.13/hooks.js';
import {useCursorObserver} from '../use-cursor-observer/script.js';
const App = () => {
useCursorObserver(({
cursorRect: {bottom, left},
parentRect: {top: eTop, left: eLeft},
elements,
}) => {
setPosition({top: Math.round(bottom - eTop), left: Math.round(left - eLeft)});
});
return html`
<${DropdownMenu} position="${position}" selected="${0}"
messages="${html<span>This is a message.</span><span>Ready to select</span>}">
${items.map((item, index) => html<a key="${index}">${item}</a>)}
<//>
`;
};
export function mount() {
const app = document.createElement('div');
app.dataset.userscriptName= 'dropdown-test';
document.getElementById('editor').append(app);
render(html<${App} />, app);
}