2017-02-18 00:18:31 -08:00
|
|
|
const getCaretCoordinates = require('textarea-caret');
|
|
|
|
const riot = require('riot');
|
|
|
|
|
|
|
|
/**
|
|
|
|
* オートコンプリートを管理するクラス。
|
|
|
|
*/
|
|
|
|
class Autocomplete {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 対象のテキストエリアを与えてインスタンスを初期化します。
|
|
|
|
*/
|
|
|
|
constructor(textarea) {
|
2017-02-18 15:09:55 -08:00
|
|
|
// BIND ---------------------------------
|
|
|
|
this.onInput = this.onInput.bind(this);
|
|
|
|
this.complete = this.complete.bind(this);
|
|
|
|
this.close = this.close.bind(this);
|
|
|
|
// --------------------------------------
|
|
|
|
|
2017-02-18 00:18:31 -08:00
|
|
|
this.suggestion = null;
|
|
|
|
this.textarea = textarea;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* このインスタンスにあるテキストエリアの入力のキャプチャを開始します。
|
|
|
|
*/
|
|
|
|
attach() {
|
|
|
|
this.textarea.addEventListener('input', this.onInput);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* このインスタンスにあるテキストエリアの入力のキャプチャを解除します。
|
|
|
|
*/
|
|
|
|
detach() {
|
|
|
|
this.textarea.removeEventListener('input', this.onInput);
|
|
|
|
this.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* [Private] テキスト入力時
|
|
|
|
*/
|
|
|
|
onInput() {
|
|
|
|
this.close();
|
|
|
|
|
|
|
|
const caret = this.textarea.selectionStart;
|
|
|
|
const text = this.textarea.value.substr(0, caret);
|
|
|
|
|
|
|
|
const mentionIndex = text.lastIndexOf('@');
|
|
|
|
|
|
|
|
if (mentionIndex == -1) return;
|
|
|
|
|
|
|
|
const username = text.substr(mentionIndex + 1);
|
|
|
|
|
|
|
|
if (!username.match(/^[a-zA-Z0-9-]+$/)) return;
|
|
|
|
|
|
|
|
this.open('user', username);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* [Private] サジェストを提示します。
|
|
|
|
*/
|
|
|
|
open(type, q) {
|
|
|
|
// 既に開いているサジェストは閉じる
|
|
|
|
this.close();
|
|
|
|
|
|
|
|
// サジェスト要素作成
|
2017-02-18 14:52:06 -08:00
|
|
|
const tag = document.createElement('mk-autocomplete-suggestion');
|
2017-02-18 00:18:31 -08:00
|
|
|
|
|
|
|
// ~ サジェストを表示すべき位置を計算 ~
|
|
|
|
|
|
|
|
const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
|
|
|
|
|
|
|
|
const rect = this.textarea.getBoundingClientRect();
|
|
|
|
|
|
|
|
const x = rect.left + window.pageXOffset + caretPosition.left;
|
|
|
|
const y = rect.top + window.pageYOffset + caretPosition.top;
|
|
|
|
|
2017-02-18 14:52:06 -08:00
|
|
|
tag.style.left = x + 'px';
|
|
|
|
tag.style.top = y + 'px';
|
2017-02-18 00:18:31 -08:00
|
|
|
|
|
|
|
// 要素追加
|
2017-02-18 14:52:06 -08:00
|
|
|
const el = document.body.appendChild(tag);
|
2017-02-18 00:18:31 -08:00
|
|
|
|
|
|
|
// マウント
|
|
|
|
this.suggestion = riot.mount(el, {
|
|
|
|
textarea: this.textarea,
|
|
|
|
complete: this.complete,
|
|
|
|
close: this.close,
|
|
|
|
type: type,
|
|
|
|
q: q
|
|
|
|
})[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* [Private] サジェストを閉じます。
|
|
|
|
*/
|
|
|
|
close() {
|
2017-02-18 14:24:31 -08:00
|
|
|
if (this.suggestion == null) return;
|
2017-02-18 00:18:31 -08:00
|
|
|
|
|
|
|
this.suggestion.unmount();
|
|
|
|
this.suggestion = null;
|
|
|
|
|
|
|
|
this.textarea.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* [Private] オートコンプリートする
|
|
|
|
*/
|
|
|
|
complete(user) {
|
|
|
|
this.close();
|
|
|
|
|
|
|
|
const value = user.username;
|
|
|
|
|
|
|
|
const caret = this.textarea.selectionStart;
|
|
|
|
const source = this.textarea.value;
|
|
|
|
|
|
|
|
const before = source.substr(0, caret);
|
2017-02-27 00:01:26 -08:00
|
|
|
const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
|
2017-02-18 00:18:31 -08:00
|
|
|
const after = source.substr(caret);
|
|
|
|
|
|
|
|
// 結果を挿入する
|
2017-02-27 00:01:26 -08:00
|
|
|
this.textarea.value = trimmedBefore + '@' + value + ' ' + after;
|
2017-02-18 00:18:31 -08:00
|
|
|
|
|
|
|
// キャレットを戻す
|
|
|
|
this.textarea.focus();
|
|
|
|
const pos = caret + value.length;
|
|
|
|
this.textarea.setSelectionRange(pos, pos);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Autocomplete;
|