generated at
ICompletion
external-completionemoji-completionで使う補完機能の共通部分

2021-05-08
15:29:17 コピペしやすいように相対パスに変えた
2021-01-10
21:17:54 うまく動いていなかったとこを直した
ハリボテ気味
19:44:44 mobile版scrapboxでも起動できるようにした
scrapbox-insert-textをmobileでも動くようにしたので、mobileで無効にする必要があった
とはいえCPUに負荷がかかるので、defaultでOFFになるようにしてある

2020-12-03 00:41:42
2020-11-12 19:47:34
mobile版scrapboxで起動しないようにした
2020-11-02 10:27:24
補完を発動する対象が空白の場合にエラーが発生して機能しなくなる問題を解決したはず
最初のトリガー文字 : しか入っていないかどうかを判定する必要がある
トリガー文字を使って処理をしているのはLinkObserverだから、こいつの仕事としたほうがよさそう
検索処理に、空白が入ったときの対処を入れればいいや。そっちのほうがまともそう
そもそも、検索keywordに空白を入れたときにエラーを吐いて止まること自体おかしい
2020-10-19 20:00:20
マウス操作でエラーが出ないようにした
操作自体に支障はないのだが、consoleにエラーが書き込まれるのが気持ち悪かったので対処した
2020-10-16 06:13:28
遅延読み込み用propertyを追加した
2020-10-11 23:36:04
キー入力でない場合は、補完を開始しないようにした
リンク入力補完 (scrapbox)と挙動を合わせた
入力候補の検索は事前に走らせておく
23:43:35 実装が難しかった
補完中かどうかをflag管理することにした

以下、本体のscript
hr
以下をimportする
script.js
import {suggestWindow} from '../suggestWindow/script.js'; import {LinkObserver} from '../LinkObserver/script.js'; import {singletonWorker} from '../singletonWorker/script.js'; import {getCharPositionUnderCursor} from '../getCharPositionUnderCursor/script.js'; import {insertText} from '../scrapbox-insert-text/script.js'; import {press} from '../scrapbox-keyboard-emulation-2/script.js'; import {cursor} from '../scrapbox-cursor-position-3/script.js'; import {line as l} from '../scrapbox-line-info-2/script.js'; import {isMobile} from '../mobile版scrapboxの判定/script.js';

二重起動していたら、古い方を削除して再読込するようにしよう
projectごとに読み込む補完候補が違うときに必要
再読込できなかったので諦める
script.js
export class ICompletion { constructor({ id, projects = [], projects_lazy = [], includeYourProject, maxSuggestionNum, maxHeight, trigger, makeRaw, searchWorkerCode, enableOnMobile = false, disableKeybindings = false,}) { // 既に同種の補完機能が起動している場合は何もしない // mobile版でも起動しない if(document.getElementById(id) || (!enableOnMobile && isMobile())) { this.alreadyExists = true; return; } this.alreadyExists = false; this.isCompletioning = false; // 入力補完中かどうか this._disableKeyBindings = disableKeybindings; // keyboard shortcutsを無効にする this._disableCompletion = false; // 補完を無効にする this.window = new suggestWindow({id: id, maxHeight: maxHeight}); this.observer = new LinkObserver(this.window.editor,{trigger: trigger}); if(includeYourProject) { // 重複を取り除く this.projects = [...new Set([...projects, scrapbox.Project.name])]; this.projects_lazy = [...new Set(projects_lazy)]; } else { // 重複を取り除く this.projects = [...new Set(projects .filter(project => project != scrapbox.Project.name))]; this.projects_lazy = [...new Set(projects_lazy .filter(project => project != scrapbox.Project.name))]; }

constructor で非同期関数は使えないので注意
ここでは _importEmojis() constructor() から start() に移動している
script.js
// 入力候補のリストは子クラスで準備する this.searchWorker = new singletonWorker(searchWorkerCode); this.maxSuggestionNum = maxSuggestionNum; // 検索結果を保持するリスト this.matchedList = []; // this._log()で使う this.logHeadString = id; }

メソッドを addEventListener に渡すときは this を固定する必要がある
↑この方法を使った
script.js
//入力補完機能を起動する start() { //二重起動防止装置 if(this.alreadyExists) return; this._importDataList(); this.window.editor.addEventListener('keyup', e => this._keyupEventHandler(e)); this.window.editor.addEventListener('mouseup', e => this._keyupEventHandler(e)); this.window.editor.addEventListener('keydown', e => this._keydownEventHandler(e)); this.searchWorker.addEventListener('message', m => this._updateList(m)); }

入力補完の状態切り替え
入力補完を開始すると、windowを開く
終了すると、windowを閉じる
script.js
_startCompletion(cursor) { this.isCompletioning = true; if (this.matchedList.length === 0) return; this.window.reDraw(cursor); this.window.open(); } _stopCompletion(cursor) { this.isCompletioning = false; this.window.close(); }

入力補完候補を非同期に読み込む
await で待たないようにしたので、ページの読み込み途中でも補完を使用できる
script.js
// 入力候補を非同期に読み込む // 実装は子クラスに任せる async _importDataList() { return; }

押したキーを離したときに発火する関数
script.js
_keyupEventHandler(e) { if (!e.key && e.button === undefined) return; if (this._disableCompletion) return; //console.log(`${e.key} is up`); if(this._isInCodeBlock()) { this._stopCompletion(); return; } switch(e.key) { case 'Escape': case 'Home': case 'End': case 'PageUp': case 'PageDown': this._stopCompletion(); return; } // 監視対象である、userが入力中の外部プロジェクトリンクに変更があれば、入力候補を更新する const cursor = document.getElementById('text-input'); if (this.observer.reload(cursor)) { if(!this.observer.target) { this._stopCompletion(); return; } this._log('target: %o', this.observer.target);
terminate() を使うとWebWorkerが死んでしまう。
新しく作成する必要がある
どういう仕組かよくわからなかったので、代わりに原始的なflag管理で実装した
/nishio/単一責任の原則に基づいて、別のclassに分離したい
した
script.js
this._postMessage(this.observer.target); } // 補完対象がない場合はwindowを閉じる if(!this.observer.target) { //console.log('No target link is found.'); this._stopCompletion(); return; } // マウス操作のときは何もしない if (!this.isCompletioning && !e.key) { return; } // キー入力でないときは、windowが表示されていない限り何もしない if (!this.isCompletioning && !e.key.match(/^.$/) && e.key !== 'Backspace') { //console.log('Window is opened only when character keys or Backspace key are pressed'); return; } // 候補が一つしかないときはそれを入力する if(this.window.length == 1 && this.window.hasFocus() && !this._disableKeyBindings) { this.focusedItem.click(); return; } this._startCompletion(cursor); }

キーを押したときに発火する関数
default動作を握りつぶしたいキーや押した瞬間に何かをしたいキーに使う
script.js
_keydownEventHandler(e) { if (this._disableCompletion) return; if (!e.key) return; // 入力補完windowが表示されていないときは何もしない if(!this.window.isOpen()) return; //console.log(`${e.key} is pressed`); // 候補がなかったらwindowを閉じる // flagは立てたままにする if(this.matchedList.length == 0) { this.window.close(); return; } // 文字入力の場合はfocusをtext-inputに戻す if(e.key.match(/^.$/) && this.window.hasFocus()) { document.getElementById('text-input').focus(); return; } // 以降のkeyboard shortcutsはフラグが経っていないときのみ有効にする if (this._disableKeyBindings) return; // focusをwindowに移す // Tabを押さないと選択に移れないようにした // 補完したくないときに↑↓キーを押してwindowに入ってしまうのが煩わしかった if((e.key == 'Tab' && e.shiftKey) || (e.key == 'ArrowUp' && this.window.hasFocus())) { e.stopPropagation(); e.preventDefault(); this.window.selectPreviousItem({wrap: true}); return; } if(e.key == 'Tab' || (e.key == 'ArrowDown' && this.window.hasFocus())) { e.stopPropagation(); e.preventDefault(); this.window.selectNextItem({wrap: true}); return; } if(e.key == 'Enter') { e.stopPropagation(); e.preventDefault(); if(this.window.hasFocus()) { // 選択項目で確定する this.window.focusedItem.click(); } else { // 一番先頭の候補で確定する this.window.firstItem.click(); } return; } }

入力候補を更新する関数
WebWorkerの処理が終わるたびに実行される
script.js
_updateList(message) { if (!message.data) return; this.matchedList = message.data; //console.log(`Got ${this.matchedList.length} matched items`); //補完windowに表示する項目を作成する this.window.resetItems(this._createGuiList(this.matchedList)); // 入力補完中のときのみwindowを出す if(!this.isCompletioning) return; const cursor = document.getElementById('text-input'); this._startCompletion(cursor); } //補完windowに表示する項目を作成する // 実装は子クラスに任せる _createGuiList(matchedList) { return; }

入力を確定する関数
script.js
_comfirm(_, text) { // '['の文字番号を取得する const c = cursor(); const cursorIndex = c.index; const index = l(c.line).text.lastIndexOf('[',cursorIndex); const length = this.observer.raw.length; console.log({index, length}); for (let i = cursor().index; i > index; i--) { press('ArrowLeft'); } // テキストを置換する for (let i = 0; i < length; i++) { press('ArrowRight', {shiftKey: true}); } insertText({text: text}); // 補完終了 this._stopCompletion(); }

WebWorkerに検索を依頼する
script.js
// 実装は子クラスに任せる _postMessage(word) { return; }

その他のUtility系関数

code blockの中にカーソルがあるかどうか判定する
script.js
_isInCodeBlock() { const cursorLine = this.window.editor.getElementsByClassName('cursor-line')[0]; if(!cursorLine) return false; return cursorLine.getElementsByClassName('code-block').length == 1; } // debug用 _log(msg, ...objects) { console.log(`[${this.logHeadString}] ${msg}`, objects); } }

#2021-05-08 15:29:27
#2021-01-10 19:45:50
#2020-12-29 18:46:44
#2020-12-03 00:44:48
#2020-11-12 03:34:35
#2020-11-02 10:26:19
#2020-10-19 20:01:11
#2020-10-16 06:13:17
#2020-10-11 23:35:52
#2020-09-30 19:37:44