外部projectのページを入力補完するUserScript
仕様
;で補完開始
もしくは、[/で補完開始
補完先projectは予め指定しておく
補完先project listsからページを読み込み、サジェストを出す
ctrl+iを押すとiconが挿入される
利点
それでいて、入力補完に出すことも出来る
課題
日本語入力補完はどうやる?
ambigを文字コードに応じて切り替えればいいのかなあ
document.getElementById('<lineId>').textContent.trim()でその行の文字列を取得できる
そこから、/\[\//を使えば[/にマッチする文字が見つかる
複数見つかった場合
文字の座標を取得する
spanで一文字ずつ分けられている
それとcursorの座標とを照合し、一番近いものから取り出す
document.querySelector('#lineId .c-x').offsetLeftとdocument.getElementsByClassName('cursor')[0].style.leftが一致するので、そこから判定できる
機能ごとに試してみたほうがいい
キー入力するたびに、cursorの位置にある文字を取得するscript
c-xをconsole.logに出力するだけでも良さそう
もしくは、カーソルの下にwindowを表示して、そこに文字を入れていく
適当にクラスを作っておく
mouseのeventで呼び出してもいいかも
でもkeyのeventのほうがやりやすいかな?
cursor行の文字を取得するscriptは出来た
補完開始と終了のトリガー
開始
stackに積む
終了
トリガー文字のc-xを記憶しておく
c-xがトリガー文字でなくなったら、補完を中断する
↑が起こる状況
文字を消した
トリガー文字の前に何か入力した
これらのeventであれば、補完を中断してしまっても構わないだろう。
テスト
/icons/GoogleChrome.iconでは問題なく動く。
あとは/icons/firefox.iconでテストする
replaceTextにミスが有った
16:39:28 直した
16:40:40 成功!
16:40:48 tabキーで候補を選択できるようにする
16:52:42 成功!
16:58:55 候補が一つしかなくなったら自動で確定するようにした
17:06:28 userが入力し続けているとeventの実行が前後してむちゃくちゃになってしまうので、windowにfocusが当たっている場合に自動確定するようにした。
入力対象を監視するclassを作ったほうが良さそう
課題
全角文字の間にカーソルがあると置換に失敗する
数文字分ずれる
対策
カーソルキーで/の前まで移動させる?
出来るかな?
/icons/done.icon18:15:34 /icons/GoogleChrome.iconでは解決した
/icons/done.icon18:18:47 /icons/firefox.iconでも動くことを確認した
code:script.js
import {suggestWindow} from '/api/code/takker/suggestWindow/script.js';
import {fizzSearch} from '/api/code/takker/fizzSearch/script.js';
import {importExternalPageName} from '/api/code/takker/importExternalPageName/script.js';
import {externalLinkObserver} from '/api/code/takker/externalLinkObserver/script.js';
export async function startSuggestingExternalProjectLinks(projectNames, maxSuggestionNum = 30) {
const suggestion=new suggestWindow();
const linkObserver = new externalLinkObserver(suggestion.editor);
const titles = await importExternalPageName(projectNames);
// windowの更新
const eventHandler = e => {
if (!e.key) return;
//console.log(${e.key} is up.);
const cursor = document.getElementById('text-input');
// code blockの頭とcode blockの中身では動作しないようにする
if(suggestion.editor.getElementsByClassName('cursor-line').length != 0) {
if (suggestion.editor.getElementsByClassName('cursor-line')0.textContent.trim() == 'code:' || suggestion.editor.getElementsByClassName('cursor-line code-block').length == 1) {
//console.log('disable suggestion');
suggestion.close(cursor);
return;
}
}
//console.log(focused item: ${suggestion.getFocusedItem().textContent});
if (linkObserver.reload(cursor)) {
const suggestTitles = updateSuggestList(titles, linkObserver, linkObserver, cursor);
if(!suggestTitles) {
suggestion.close();
return;
}
suggestion.updateItems(
createGuiList(suggestTitles.slice(0, maxSuggestionNum), suggestion, linkObserver, cursor));
}else{
//console.log('skip updating list');
}
if(!linkObserver.linkString) {
suggestion.close();
return;
}
// 候補が一つしかないときはそれを入力する
if(suggestion.getItems().length == 1 && suggestion.hasFocus()) {
comfirmSuggestion(suggestion, linkObserver, cursor
, suggestion.getItems()0.textContent); }
switch(e.key) {
case 'Escape':
case 'Home':
case 'End':
case 'PageUp':
case 'PageDown':
suggestion.close();
return;
}
if(!suggestion.hasFocus()){
const cursorIndex = getCharPositionUnderCursor(suggestion.editor,cursor);
console.log(targetLink: ${linkObserver.linkString});
console.log(left: ${cursorIndex - linkObserver.leftCharIndex -1}, cursor: ${cursorIndex}, right: ${linkObserver.rightCharIndex - cursorIndex});
}
suggestion.reDraw(cursor);
suggestion.open();
};
const eventHandler2 = e => {
if (!e.key) return;
//console.log(${e.key} with ${e.shiftKey} is down)
if((e.key == 'ArrowUp'
|| (e.key == 'Tab' && e.shiftKey))
&& (suggestion.isOpen() && suggestion.hasFirstItemFocus())) {
e.stopPropagation();
e.preventDefault();
suggestion.close();
return;
}
if((e.key == 'ArrowDown' || e.key == 'Tab')
&& (suggestion.isOpen() && !suggestion.hasFocus())) {
e.stopPropagation();
e.preventDefault();
suggestion.selectFirstItem();
return;
}
};
suggestion.editor.addEventListener('keydown', eventHandler2);
suggestion.editor.addEventListener('keyup', eventHandler);
suggestion.editor.addEventListener('mouseup', eventHandler);
}
code:script.js
function updateSuggestList(sourceList, linkObserver, cursor){
if(!linkObserver.linkString) return undefined;
const tergetString = linkObserver.linkString.slice(1,linkObserver.linkString.length - 1);
const suggestTitles = fizzSearch(tergetString, sourceList);
if(suggestTitles.length == 0) return undefined;
// 補完候補と既に入力されたリンクが同じであれば何もしない
if(suggestTitles.length == 1 && suggestTitles0 == tergetString) { return undefined;
}
return suggestTitles;
}
code:script.js
function comfirmSuggestion(suggestion, linkObserver, cursor, insertText) {
cursor.focus();
// 1を足さないとreplaceTextで何故かずれる
const cursorIndex = getCharPositionUnderCursor(suggestion.editor,cursor) + 1;
replaceText(cursor, cursorIndex - linkObserver.leftCharIndex - 1, linkObserver.rightCharIndex - cursorIndex, insertText);
suggestion.close();
}
code:script.js
function createGuiList(suggestTitles, suggestion, linkObserver, cursor) {
//入力補完windowに表示する項目を作成する
return suggestTitles
.map(title => {
const a = document.createElement('a');
a.setAttribute('tabindex', '0');
a.setAttribute('role', 'menuitem');
const div = document.createElement('div');
div.textContent = title;
a.appendChild(div);
a.onclick = () => comfirmSuggestion(suggestion, linkObserver, cursor, title);
a.onkeypress = e => {
if (e.key !== "Enter") return;
e.stopPropagation();
e.preventDefault();
comfirmSuggestion(suggestion, linkObserver, cursor, title);
};
return a;
});
}
code:script.js
// cursor直下の文字の先頭からの要素番号を返す
function getCharPositionUnderCursor(editor,cursor){
const cursorLine = editor.getElementsByClassName('cursor-line')0; const text = cursorLine.textContent;
if (text === '' ) return undefined;
// 要素の左端の相対座標を取得する
const getLeftPosition = target => target.getBoundingClientRect().left - cursorLine.getBoundingClientRect().left;
// テキストから、それぞれの文字の座標を入れた配列を作る
const charPostions = text.split('').map((char,index) =>
[getLeftPosition(cursorLine.getElementsByClassName(c-${index})0), index]); const charDistances = charPostions
.map(pos => [Math.abs(parseInt(cursor.style.left) - pos0), pos1]) const targetIndex = charDistances01; return targetIndex;
}
code:script.js
function replaceText(cursor,left,right, insertText) {
const isFirefox = () => {
const userAgent = window.navigator.userAgent.toLowerCase();
if (userAgent.indexOf('firefox') != -1) {
return true;
}
return false;
};
console.log(Delete: ${right}chars);
console.log(Backspace: ${left}chars);
setTimeout(() => {
for(const _ of range(left)) {
cursor.dispatchEvent(new KeyboardEvent('keydown',
{bubbles: true, cancelable: true, keyCode: 8}));
}
for(const _ of range(right)) {
cursor.dispatchEvent(new KeyboardEvent('keydown',
{bubbles: true, cancelable: true, keyCode: 46}));
}
if (isFirefox()) {
const start = cursor.selectionStart; // in this case maybe 0
cursor.setRangeText(insertText);
cursor.selectionStart = cursor.selectionEnd = start + insertText.length;
const uiEvent = document.createEvent('UIEvent');
uiEvent.initEvent('input', true, false);
cursor.dispatchEvent(uiEvent);
} else {
document.execCommand('insertText', false, insertText);
}
}, 50);
}