fuzzy-select-menu@0.1.0
仕様
入力欄の上でスクロールすると項目を変えられる
検索キーワードは別に保持して、上書きしないようにする
何かを入力したら書き換える
項目は検索して絞り込んだものをそのまま使う
上下キーでも候補から選択できる
候補の確定
検索候補中の任意の項目を選んだ
クリックやエンターキーは必要ない
入力文字が既に存在する候補と一致した
確定すると入力欄が書き換わる
検索実行
入力欄の文字をuserが書き換えた
選択項目から選んだ結果書き換わった場合は何もしない
表示非表示の切り替え
入力欄[をクリックするか、focusを当てると切り替える
非表示
userが候補と入力欄以外を押した
エンターキーやクリックで候補を選んだ
表示
userが入力欄の文字を書き換える
候補がない時
表示状態であればnot foundというメッセージを出す
非表示状態なら何もしない
UI
まあスタイルとかは変えやすくはある
hookにするとしたら、どんな機能を提供するのか
出力
確定した候補
入力候補のリスト
onClick()つき
選択用函数
onInput()
入力
リスト
キー付きで渡す
検索用文字列
リストから検索対象の文字列を生成するための函数
キーバインドは……別にしてもいいか?
これだけだとhookにする意味がない
hookに入れないもの
開閉判定
<DropdownMenu>の座標計算
既知の問題
初期値の設定ができない
本当に初回だけを設定したい
listの形式を固定したくない
hookにすればまだやりようはありそうなのだが……
mobileでproject候補のクリックがうまくいかない
backgroundを押していると判定される?
<span>ではなく<button>にして、click eventを発火させられるようにしよう
2021-06-27 03:26:52 応急処置として、どこの要素にもfocusが当たっていないときのみ、blurで補完windowを消すようにする
実装したいこと
スワイプで候補を切り替えたい
2021-06-27
<a>タグに戻した
click eventを使うようにした
2021-06-24
09:03:17 mobileだとclick eventを<span>に対して発生させられないみたい
07:43:05 補完windowが閉じている場合は、<tab>で別のcomponentにfocusを移せるようにする
07:38:58 入力欄からfocusが外れたら補完windowを閉じる
07:24:12 入力補完候補の表示位置を直した
06:30:01 だいたい実装終了
入力候補上でもスクロールすると候補選択できるようにしたかったが、断念した
他の操作は大体チェックした
2021-06-22
2021-06-19
2021-06-13
2021-06-01
同じコードを何度も書きたくない
02:47:06 大体できた
2021-05-31
いや、Hookに切り出したほうがいいかも
Componentを自由に作れる
code:script.js
import {html} from '../htm@3.0.4%2Fpreact/script.js';
import {DropdownMenu} from '../dropdown-container@0.1.1/script.js';
import {useState, useCallback, useEffect} from '../preact@10.5.13/hooks.js';
import {useSelect} from '../use-select@0.1.0/script.js';
import {useSearch} from '../use-search@0.1.0/script.js';
export const FuzzySelect = ({list: _initialList, onSelect, convert}) => {
// DropDownの座標計算
const adjust = useCallback(ref => {
const {left= 0, bottom = 0} = ref?.current?.getBoudingClientRect?.() ?? {};
setPosition({top: bottom, left});
}, []);
const list = useSearch({query, list: _initialList, convert});
const {
item,
selectPrev,
selectNext,
select,
blur,
} = useSelect({list});
useEffect(() => {
if (!item) return;
onSelect?.(item);
setDisplay(item.text);
// 開閉判定・表示する文字列の更新・検索文字列の更新
const onInput = useCallback(({target: {value}}) => {
setDisplay(value);
setOpen(true);
if (item?.text === value) return;
blur();
setQuery(value);
const onClick = () => setOpen(old => !old);
const onBlur = () => !document.activeElement && setOpen(false);
const onKeyDown = useCallback(e => {
const {key, shiftKey} = e;
if (key === 'Escape') {
e.preventDefault();
e.stopPropagation();
setOpen(false);
return;
}
if (key === 'Enter') {
e.preventDefault();
e.stopPropagation();
if (!item) {
select(0);
}
setOpen(false);
return;
}
// windowが開いていなければ、focusを別のcomponentに移す
if (key === 'Tab' && !open) return;
if (key === 'ArrowUp' || (key === 'Tab' && shiftKey)) {
e.preventDefault();
e.stopPropagation();
selectPrev();
return;
}
if (key === 'ArrowDown' || (key === 'Tab' && !shiftKey)) {
e.preventDefault();
e.stopPropagation();
selectNext();
return;
}
const onWheel = useCallback(e => {
e.preventDefault();
e.stopPropagation();
const {deltaY} = e;
if (deltaY > 0) {
selectNext();
return;
}
if (deltaY < 0) {
selectPrev();
return;
}
const onClickItem = useCallback((index) => {
select(index);
setOpen(false);
}, []);
return html`<span style="position: relative;">
<input
ref="${adjust}"
type="text"
value="${display}"
onInput="${onInput}"
onBlur="${onBlur}"
onClick="${onClick}"
onKeyDown="${onKeyDown}"
onWheel="${onWheel}"
/>
${open && html`<${DropdownMenu} position="${({})}"
messages="${list.length === 0 && html<span class="message">Not found</span>}"
selected="${item?.key}">
${list.map(
({key, text}, index) => html`
<a key="${key}" onClick="${() => onClickItem(index)}">${text}</a>
`
)}
<//>`}
</span>`;
};
custom hook
code:script.js
export function useFuzzySelect(props) {
const {query, list: initialList = [], convert} = props ?? {};
const list = useSearch({query, list: initialList, convert});
const {item, selectPrev, selectNext}
= useSelect({
list,
defaultSelected: 0,
});
}
test code
code:js
import('/api/code/programming-notes/fuzzy-select-menu@0.1.0/test.js');
code:test.js
import {FuzzySelect} from './script.js';
import {useState} from '../preact@10.5.13/hooks.js';
import {html, render} from '../htm@3.0.4%2Fpreact/script.js';
import {getProjectInfo} from '../scrapboxのproject情報を一括して取得するUserScript/script.js';
const App = ({list}) => {
return html`
<span>Selected item is ${result}</span>
<br />
<${FuzzySelect}
list="${list}"
convert="${({text, key}) => ${text} ${key}}"
onSelect="${({text, key}) => setResult(/${key} ${text})}"
/>
`;
}
(async () => {
const watchListIds = Object.keys(JSON.parse(localStorage.getItem('projectsLastAccessed')));
.map(({name, displayName}) => ({text: displayName, key: name}))
.sort((a, b) => a.text.localeCompare(b.text));
const app = document.createElement('div');
app.dataset.userscriptName= 'fuzzy-select-test';
document.getElementById('editor').append(app);
app.attachShadow({mode: 'open'});
render(html<${App} list="${watchList}"/>, app.shadowRoot);
})();