ICompletion
2021-05-08
15:29:17 コピペしやすいように相対パスに変えた
2021-01-10
21:17:54 うまく動いていなかったとこを直した
ハリボテ気味
とはいえCPUに負荷がかかるので、defaultでOFFになるようにしてある
2020-12-03 00:41:42
2020-11-12 19:47:34
2020-11-02 10:27:24
補完を発動する対象が空白の場合にエラーが発生して機能しなくなる問題を解決したはず
最初のトリガー文字 :
しか入っていないかどうかを判定する必要がある
検索処理に、空白が入ったときの対処を入れればいいや。そっちのほうがまともそう
そもそも、検索keywordに空白を入れたときにエラーを吐いて止まること自体おかしい
2020-10-19 20:00:20
マウス操作でエラーが出ないようにした
操作自体に支障はないのだが、consoleにエラーが書き込まれるのが気持ち悪かったので対処した
2020-10-16 06:13:28
遅延読み込み用propertyを追加した
2020-10-11 23:36:04
キー入力でない場合は、補完を開始しないようにした
入力候補の検索は事前に走らせておく
23:43:35 実装が難しかった
補完中かどうかをflag管理することにした
以下、本体のscript
以下をimportする
script.jsimport {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.jsexport 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);
新しく作成する必要がある
どういう仕組かよくわからなかったので、代わりに原始的なflag管理で実装した
した
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;
}
}
入力候補を更新する関数
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();
}
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);
}
}