Skip to content
Snippets Groups Projects
autocomplete.js 2.9 KiB
Newer Older
syuilo's avatar
:v:
syuilo committed
const getCaretCoordinates = require('textarea-caret');
const riot = require('riot');

/**
 * オートコンプリートを管理するクラス。
 */
class Autocomplete {

	/**
	 * 対象のテキストエリアを与えてインスタンスを初期化します。
	 */
	constructor(textarea) {
		this.suggestion = null;
		this.textarea = textarea;
syuilo's avatar
syuilo committed

		this.onInput = this.onInput.bind(this);
		this.complete = this.complete.bind(this);
		this.close = this.close.bind(this);
syuilo's avatar
:v:
syuilo committed
	}

	/**
	 * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。
	 */
	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();

		// サジェスト要素作成
		const tag = document.createElement('mk-autocomplete-suggestion');
syuilo's avatar
:v:
syuilo committed

		// ~ サジェストを表示すべき位置を計算 ~

		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;

		tag.style.left = x + 'px';
		tag.style.top = y + 'px';
syuilo's avatar
:v:
syuilo committed

		// 要素追加
		const el = document.body.appendChild(tag);
syuilo's avatar
:v:
syuilo committed

		// マウント
		this.suggestion = riot.mount(el, {
			textarea: this.textarea,
			complete: this.complete,
			close: this.close,
			type: type,
			q: q
		})[0];
	}

	/**
	 * [Private] サジェストを閉じます。
	 */
	close() {
syuilo's avatar
syuilo committed
		if (this.suggestion == null) return;
syuilo's avatar
:v:
syuilo committed

		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);
		const trimedBefore = before.substring(0, before.lastIndexOf('@'));
		const after = source.substr(caret);

		// 結果を挿入する
		this.textarea.value = trimedBefore + '@' + value + ' ' + after;

		// キャレットを戻す
		this.textarea.focus();
		const pos = caret + value.length;
		this.textarea.setSelectionRange(pos, pos);
	}
}

module.exports = Autocomplete;