generated at
emoji-completion
/customize/emoji-selector非同期で動くようにしたUserScript
warning開発版につき、破壊的変更を行う可能性あり
ドッグフーディングして、不具合を大体解消できたら、/customizeにも掲載する。
2022-02-03 13:52:55 不安定っぽいので非推奨です
メンテも1年以上していませんし今後もメンテするつもりはないです
修正点を指摘されたら直すかも

hr
改善点
全角文字を入力補完できるようになった
入力候補の絞り込みを非同期化した
WebWorkerを使って並列処理する
描画が固まらなくなったと思う。

機能の変更
[:] の内部で補完開始
: だけだと実装が面倒だった
: からどの文字までをfizzSearchに使う文字列にしたらいいかがわからなかった
[:] ならStringObserverの実装を流用出来るので簡単
候補が一つでも、windowにfocusを当てない限り自動入力しない
projectごとに、入力候補に追加するページの条件を設定できるようにした
やめた。いらない気がする
補完windowにfocusが移っている状態で文字を入力すると、即座にcursorにfocusを戻すようにした
Vimiumなどを有効にしているとキーが干渉するので注意

操作説明
[:] の中にカーソルを置くと補完開始

候補の選択
or Tab キーで前候補を選択
or Shift+Tab キーで次候補を選択
Enter or クリックで候補を確定
windowからカーソルにfocusを戻す
文字を入力するとカーソルにfocusが戻る
Esc Home End PageUp PageDown を押すと補完を中断する
2021-01-09 17:25:52 うまく動いていない
入力候補が一つだけなった場合、それを選択すると自動で入力される
2021-01-09 17:27:03 うまく動いていない
入力途中で Enter を押すと、最初の入力候補が入力される

入力補完に使用する文字列の取得
[:$] $ の部分を取得する
補完終了の条件
候補がない
[:] の中にいない

導入方法
impot.jsをコピーし、自分のページのscript.jsに貼り付ける
import.js
import {EmojiCompletion} from '/api/code/takker/emoji-completion/script.js'; const emojiCompletion = new EmojiCompletion({ projects: ['icons','emoji','icons2'],
Optionの説明
projects : ここに列挙したprojectsのページを入力候補とする。
必須変数
includeYourProject : このUserScriptを使用するprojectのpageも入力候補に加えるかどうか
default: true
maxSuggestionNum : 一度に表示する入力候補の最大数
default: 30
maxHeight : 入力補完windowの最大の高さ
この値に入力候補が入り切らなかった場合は、スクロール表示に切り替わる
default: calc(50vh - 100px)
enableOnMobile : mobile版scrapboxでも有効にするかどうか
default: false
import.js
// includeYourProject: true, // maxSuggestionNum: 30, // maxHeight: `calc(50vh - 100px)` // enableOnMobile: false, }); emojiCompletion.start();

更新履歴
2021-01-10 19:46:06 mobile版scrapboxでも動くようにした
enableOnMobile true にする
挙動がまだ怪しい
何回か文字入力しないと補完windowが出てこない
確定時にガクガクする
仮想キーボードが同時に消えて再描画が走るからではないか?
一応置換は成功している
2020-11-12 03:44:13 debug文字列を変えた
2020-10-15 15:53:27
一致度順に検索結果が出てくるようにした
検索候補をEmojiCompletionではなくcompute.jsで持つようにした
検索候補そのものはEmojiCompletionで使わない
compute.jsに直接持たせることで、検索候補のcopyをせずに済むようになった
WebWorkerで検索候補の読み込みを行うようにした
2020-10-12 01:49:24
同じ文字列長のemojiは辞書順に並べ替えるようにした
一部のemojiが補完候補に出てこない問題を解決した
検索wordの先頭に : が入ったままだったのが原因
2020-09-30 19:36:45
LinkObserverを導入した
2020-09-17
15:39:24
検索する毎に検索結果をsortするようにした
14:18:59
自分のprojectのiconの場合は、project pathを含めないアイコン記法を入力するようにした
09:10:50
external-completionと共通する部分をICompletionとして分離した。
2020-09-14 17:56:52
入力確定の Enter が正常に作動するようにした
<a> タグがあるから、 .childlen[0] を間に挟まないといけなかった
2020-09-12 11:02:47
二重起動防止装置にミスが有ったので修正
2020-09-09 00:13:47
コードブロック記法で入力補完が起動しないようにした
_isInCodeBlock を修正
2020-09-08 23:56:28
2020-09-01 10:12:31 recyclerefactoring
Global函数を別のmoduleに分離した
2020-08-28 12:30:28 recyclerefactoring
singletonWorkerを採用した

既知の問題
一部プロジェクトで何故か機能しない
やはり同じ問題が発生している
今のところはreloadして応急処置するしかない
ctrl + i で入力したい
ICompletionをいじる必要があるが、それをやるとexternal-completion-2に影響が出てしまう

以下、本体のscript
hr
以下をimportする
script.js
import {ICompletion} from '/api/code/takker/ICompletion/script.js';

script.js
export class EmojiCompletion extends ICompletion { constructor({projects, includeYourProject = true, maxSuggestionNum = 30, maxHeight = 'calc(50vh - 100px)', enableOnMobile = false} = {}) { super({ id: 'emoji-completion', projects: projects, includeYourProject, maxSuggestionNum, maxHeight, trigger: /^:/, makeRow: string => string.substr(2, string.length - 3), searchWorkerCode: '/api/code/takker/emoji-completion/searchWorker.js', enableOnMobile }); }

emojiを非同期に読み込む
await で待たないようにしたので、ページの読み込み途中でも補完を使用できる
script.js
// emojiの入力候補を非同期に読み込む // 読み込み途中でも補完は有効 async _importDataList() { this.searchWorker.postMessage({loading: true, projects: this.projects}); }

入力候補を更新する関数
WebWorkerの処理が終わるたびに実行される
script.js
//補完windowに表示する項目を作成する _createGuiList(matchedList) { const cursor = document.getElementById('text-input'); return matchedList .map(emoji => { const div = document.createElement('div'); div.textContent = `/${emoji.project}/${emoji.name}`; const img = document.createElement('img'); img.src = `/api/pages/${emoji.project}/${encodeURIComponent(emoji.name)}/icon`; img.classList.add('icon'); img.style.height = '17px'; img.style.float = 'left'; const insertText = `[${scrapbox.Project.name !== emoji.project ? '/'+ emoji.project + '/' : ''}${emoji.name}.icon]` return new Object({ elements: [img, div], onComfirm: () => this._comfirm(cursor, insertText), }); }); }

WebWorkerに検索を依頼する
script.js
_postMessage(word) { this.searchWorker.postMessage({word: word, maxSuggestionNum: this.maxSuggestionNum}); } }

WebWorkerが処理するscript

searchWorker.js
const range = n => [...Array(n).keys()]; // [0,1,...,n-1]を作る函数 // list: 補完候補が入ったリスト const workerNum = navigator.hardwareConcurrency; const workers = range(workerNum).map(_ => new Worker( '/api/code/takker/emoji-completion/compute.js'));
backgroundでfizzSearchを行う
並列で検索を行う
searchWorker.js
// 曖昧検索をする function searchWord(message) { const word = message.data.word.replace(/^:(.*)/,'$1'); // :をとる const jobs = workers .map(worker => { // 処理を投げる const job = new Promise((resolve, reject) => worker.addEventListener('message', message => { resolve(message.data); })); worker.postMessage({word: word, limit: message.data.maxSuggestionNum}); return job; }); // 処理が帰って来たら結果をPOSTする Promise.all(jobs).then(results => { // まず、listの順番を復元する const temp = results .sort((a, b) => a.index - b.index) .map(result => result.emojis); // 曖昧度順に並べ直す const max = temp.map(emojis => emojis.length).reduce((r,c) => Math.max(r,c)); const matchedLinks = range(max) .flatMap(i => temp.flatMap(emojis => emojis[i] ?? [])) .slice(0, message.data.maxSuggestionNum); self.postMessage(matchedLinks); }); }

外部projectからアイコンを読み込む
2020-09-18 17:06:01 全部読み込み終わったら、文字数順にsortする
2020-10-12 01:27:51 同じ文字数のemojiは辞書順に並べる
searchWorker.js
async function addEmojis(projects) { let emojis = await Promise.all(projects.flatMap(project => fetchEmojis(project))) .then(lists => lists.flat());
searchWorker.js
emojis.sort((a, b) => a.name.length === b.name.length ? a.name.localeCompare(b.name) : a.name.length - b.name.length); // workerの数だけ分割して渡す const skip = Math.floor(emojis.length/workerNum) + 1; for (const i of range(workerNum)) { const dividedList = emojis.slice(i * skip,(i + 1) * skip); workers[i].postMessage({loading: true, list: dividedList, index: i, // listの順番を保存する }); } // 処理が終わったことを伝える self.postMessage(undefined); } async function fetchEmojis(project) { // projectの全ページ数を取得する const pageNum = await fetch(`/api/pages/${project}/?limit=1`) .then(response => response.json()) .then(json => parseInt(json.count)); // pageNum = 2347 => maxIndex = 3 const maxIndex = Math.floor(pageNum / 1000) + 1; const emojis = []; const promises = range(maxIndex).map(async (index) => { const json = await fetch(`/api/pages/${project}/?limit=1000&skip=${index*1000}`) .then(res => res.json()); const pages = json.pages.filter(page => page.image !== null); pages.forEach(page => emojis.push({ name: page.title, project: project, })); }); await Promise.all(promises); _log(`Loaded ${emojis.length} emojis from /${project}`); return emojis; } // debug用 function _log(msg, ...objects) { console.log(`[search-worker@emoji-completion] ${msg}`, objects); }

WebWorkerの本体処理
parameterに応じて、読み込みを行うか検索を行うかを切り替える
searchWorker.js
self.addEventListener('message', async message => { if (message.data.loading) { addEmojis(message.data.projects); return; } searchWord(message); });

CPUごとの計算script
compute.js
self.importScripts('/api/code/takker/fizzSearch/script.js'); // 検索候補 const list = []; let index = 0; self.addEventListener('message', message => { if(message.data.loading) { index = message.data.index; _log(index, `Adding new ${message.data.list.length} suggestion items...`); list.push(...message.data.list); _log(index, 'Added.'); return; } console.log(`[Worker${index}] start searching for ${message.data.word}...`); const temp = fizzSearch({word: message.data.word, list: list, func: emoji => emoji.name, limit: message.data.limit}); console.log(`[Worker${index}] finished!`); // index: this.emojisの並び順 self.postMessage({emojis: temp, index: index}); }); // debug用 function _log(number, msg, ...objects) { console.log(`[search-worker ${number}@emoji-completion] ${msg}`, objects); }

#2022-02-03 13:53:24
#2021-01-10 19:47:14
#2021-01-09 17:32:37
#2020-12-08 01:10:05
#2020-12-03 01:39:59
#2020-11-19 08:49:18
#2020-11-12 03:36:53
#2020-10-15 15:52:42