emoji-completion
開発版につき、破壊的変更を行う可能性あり
2022-02-03 13:52:55 不安定っぽいので非推奨です
メンテも1年以上していませんし今後もメンテするつもりはないです
修正点を指摘されたら直すかも
改善点
全角文字を入力補完できるようになった
入力候補の絞り込みを非同期化した
描画が固まらなくなったと思う。
機能の変更
[:]
の内部で補完開始
:
だけだと実装が面倒だった
候補が一つでも、windowにfocusを当てない限り自動入力しない
projectごとに、入力候補に追加するページの条件を設定できるようにした
やめた。いらない気がする
補完windowにfocusが移っている状態で文字を入力すると、即座にcursorにfocusを戻すようにした
操作説明
[:]
の中にカーソルを置くと補完開始
候補の選択
↓
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
を押すと、最初の入力候補が入力される
入力補完に使用する文字列の取得
[:$]
の $
の部分を取得する
補完終了の条件
候補がない
[:]
の中にいない
導入方法
import.jsimport {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
この値に入力候補が入り切らなかった場合は、スクロール表示に切り替わる
default: calc(50vh - 100px)
default: false
import.js // includeYourProject: true,
// maxSuggestionNum: 30,
// maxHeight: `calc(50vh - 100px)`
// enableOnMobile: false,
});
emojiCompletion.start();
更新履歴
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
2020-09-17
15:39:24
検索する毎に検索結果をsortするようにした
14:18:59
自分のprojectのiconの場合は、project pathを含めない
アイコン記法を入力するようにした
09:10:50
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
refactoring
Global函数を別のmoduleに分離した
2020-08-28 12:30:28
refactoring
既知の問題
一部プロジェクトで何故か機能しない
やはり同じ問題が発生している
今のところはreloadして応急処置するしかない
ctrl
+ i
で入力したい
以下、本体のscript
以下をimportする
script.jsimport {ICompletion} from '/api/code/takker/ICompletion/script.js';
script.jsexport 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});
}
入力候補を更新する関数
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),
});
});
}
script.js _postMessage(word) {
this.searchWorker.postMessage({word: word, maxSuggestionNum: this.maxSuggestionNum});
}
}
searchWorker.jsconst 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'));
並列で検索を行う
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);
});
}
2020-09-18 17:06:01 全部読み込み終わったら、文字数順にsortする
2020-10-12 01:27:51 同じ文字数のemojiは辞書順に並べる
searchWorker.jsasync 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.jsself.addEventListener('message', async message => {
if (message.data.loading) {
addEmojis(message.data.projects);
return;
}
searchWord(message);
});
CPUごとの計算script
compute.jsself.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);
}