generated at
補完windowの選択テスト
scrapbox-suggest-containerの候補選択をテストしますtakker

見つかったバグ
フォーカスを移動できない
Shadow DOMの内部で発生したfocus eventは、custom elementそのものに対して発生したように見えてしまうようだ
対策
Element.attachShadow() delegatesFocus: true を渡す
<suggest-container-item> をLight DOMに移す
<suggest-container> <slot>を導入して、そこに <suggest-container-item> を入れる
document.activeElement.shadowRoot.activeElementに加えて、これを使わないと解決しそうにないな……
汎用性がない
<suggest-container-item> <suggest-container> 専用になってしまう。……いやまあ専用ではあるのだが、できれば依存を減らしたい
delegatesFocus: true をつかって解決させようtakker
22:47:00 これだけだと解決しない……
というか、 delegatesFocus: true を渡してもそもそもこの問題は解決しない
custom elementに対して :focus を発生させないようにすることはできない
解決した
suggest-container-item #a :focus の有無をElement.matches()で調べる
スタイルをすこし見直してこのテストは終わり
PageMenuのテストをして終了


js
(async () => { const projectName = 'programming-notes'; const pageTitle = '補完windowの選択テスト'; const promises = [ import(`/api/code/${projectName}/scrapbox-suggest-container/test-dom.js`), //import(`/api/code/${projectName}/scrapbox-suggest-container/test-dark-theme.js`), import(`/api/code/${projectName}/scrapbox-suggest-container/script.js`), ]; const promise = import(`/api/code/${projectName}/${pageTitle}/test1-project-list.js`); const worker = new Worker(`/api/code/${projectName}/${pageTitle}/test1-worker.js`); worker.postMessage({type: 'fetch', projects: (await promise).projects}); const [{editor, cursor},] = await Promise.all(promises); // 入力補完windowを作る const suggestBox = document.createElement('suggest-container'); editor.append(suggestBox); // とりあえず、cursorに追随するだけにする const observer = new MutationObserver(() =>{ suggestBox.show({ top: parseInt(cursor.style.top) + 14, left: parseInt(cursor.style.left), }); }); observer.observe(cursor, {attributes: true}); // tabキーで選択する editor.addEventListener('keydown', e => { if (this.hidden) return; if (e.key !== 'Tab') return; if (e.altKey || e.ctrlKey) return; e.preventDefault(); e.stopPropagation(); if (e.shiftKey) { suggestBox.selectPrevious({wrap: true}); } else { suggestBox.selectNext({wrap: true}); } }); // あいまい検索して、候補を入力補完windowに追加する window.search = (word, {limit = 30, timeout = 10000,} = {}) => { // 時間がかかるようであればLoading表示をする const timer = setTimeout(() => { const image = /paper-dark-dark|default-dark/ .test(document.head.parentElement.dataset.projectTheme) ? 'https://img.icons8.com/ios/180/FFFFFF/loading.png' : 'https://img.icons8.com/ios/180/loading.png'; suggestBox.pushFirst({text: 'Loading...', image,}); }, 1000); worker.postMessage({type: 'search', word, limit, timeout}); worker.addEventListener('message', ({data: {links}}) => { clearTimeout(timer); suggestBox.clear(); suggestBox.push(...links.flat().map(link => { return { text: link, link: `https://scrapbox.io${link}`, onClick: () => window.open(`https://scrapbox.io${link}`), }; })); }, {once: true}); }; })();

入力補完に使うscrapbox projects
test1-project-list.js
export const projects = [ 'hub', 'villagepump', /*'motoso', 'shokai', 'nishio', 'masui', 'rakusai', 'yuiseki', 'june29', 'ucdktr2016', 'rashitamemo', 'thinkandcreateteck', 'customize', 'scrapboxlab', 'scrasobox', 'foldrr', 'scrapbox-drinkup', 'public-mrsekut', 'mrsekut-p', 'marshmallow-rm', 'wkpmm', 'sushitecture', 'nwtgck', 'dojineko', 'kadoyau', 'inteltank', 'sta', 'kn1cht', 'miyamonz', 'rmaruon', 'MISONLN41', 'yuta0801', 'choiyakiBox', 'choiyaki-hondana', 'spud-oimo', 'keroxp', 'aioilight',*/ ];

worker code
test1-worker.js
const pageTitle = '補完windowの選択テスト'; self.importScripts('/api/code/programming-notes/WebWorker用asearch/script.js'); // 検索候補 const list = []; self.addEventListener('message', message => { switch (message.data.type) { case 'search': search(message.data); break; case 'fetch': fetch_(message.data.projects); break; } }); async function search({word, limit, timeout}) { //_log(`start searching for ${word}...: limit = ${limit}`); const result = fuzzySearch({ query: word.split('/').join(' '), source: list, limit, timeout, }); //_log('finished: %o', result); self.postMessage({links: result,}); } async function fetch_(projects) { _log('Loading links from %o', projects); const result = (await Promise.all(projects .map(project => fetchExternalLinks(project)) )).flat(); _log(`Finish loading ${result.length} links from %o`, projects); list.push(...result); } async function fetchExternalLinks(project) { let followingId = null; let temp = []; _log(`Start loading links from ${project}...`); do { _log(`Loading links from ${project}: followingId = ${followingId}`); const json = await (!followingId ? fetch(`/api/pages/${project}/search/titles`) : fetch(`/api/pages/${project}/search/titles?followingId=${followingId}`) ).then(res => { followingId = res.headers.get('X-Following-Id'); return res.json(); }); temp.push(...json.flatMap(page => [...page.links, page.title]).map(link => `/${project}/${link}`)); } while(followingId); const links = [...new Set(temp)]; // 重複を取り除く _log(`Loaded ${links.length} links from /${project}`); return links; } // debug用 function _log(msg, ...objects) { console.log(`[search-worker @${pageTitle}/test1-worker.js] ${msg}`, ...objects); }