【JavaScript】textareaでサジェスト機能を作る
↓右側のtextareaについて、選択肢を表示してそれをクリックするとタイトルがtextarea内に入力されるという挙動を作る(なお左側のリンク表示は全然別なもので今回は触れない)
https://gyazo.com/46c2356bbc0a40045dd7e129faa99112
概要
いつ
textarea要素で入力中、[[の後に文字を打った時
何が起こる
打った文字をタイトルに含むデータをデータベースから抽出し、候補としてポップアップ表示する
表示位置はカーソル位置そば
(キャレット位置の側にしたかったが、やや面倒くさいのでカーソル位置で妥協)
そしてどうする
候補の要素をマウスでクリックするか、Tabキーで選択してEnter
するとどうなる
候補のデータのタイトルが入力され、[[]]で囲われた状態になる
前提
データベースの形式について(デジタルノートツールなので1単位を「ノート」と表現)
code:x.js
// ノートのデータは以下のような形式になっているものとする
/*
DATA = [
{
title: 'ノートのタイトル',
body: '本文',
その他のキー: その他のプロパティ,
...
}, // この塊がDATA内に何十とか何百とか何千単位である状態
...
]
*/
コード(なおhidden,selected,clickableの各classについては下にCSSが書いてあります)
code:js
// 予めHTMLに<div id="popup" class="hidden"></div>を記述しておく
// ポップアップ関連の要素を簡単に取得できるようにしておく
const popup = {
area: document.getElementById('popup'),
get selected() { return document.querySelector('#popup p.selected') }, // 「選択中」という意味付けをしている子要素
get firstChild() { return document.querySelector('#popup p') }, // 子要素の先頭
}
// 常にカーソル位置を拾っておく
const mousePos = {};
document.body.addEventListener('mousemove', (e) => {
mousePos.y = e.clientY;
mousePos.x = e.clientX;
})
// textareaにイベントをセットする
const hoge = document.getElementById('hoge'); // ターゲットのtextareaを取得する
hoge.addEventListener('keydown', (e) => {
if (e.key == 'Tab' && !popup.area.classList.contains('hidden')) { // ポップアップが表示されている状態でTab
e.preventDefault(); // 通常の動作をキャンセル
if (!e.shiftKey && popup.selected) { // 「選択中」設定の子要素が存在している且つShift無し
const next = popup.selected.nextElementSibling;
if (next) { // 次の要素があれば
popup.selected.classList.remove('selected');
next.classList.add('selected'); // 次の要素を「選択中」設定にする
}
} else if (e.shiftKey && popup.selected) { // 「選択中」設定の子要素が存在している且つShift有り
const prev = popup.selected.previousElementSibling;
if (prev) { // 手前の要素があれば
popup.selected.classList.remove('selected');
prev.classList.add('selected'); // 手前の要素を「選択中」設定にする
}
} else { // 「選択中」設定の子要素が存在しない時
popup.firstChild.classList.add('selected'); // 一番上を「選択中」設定にする
}
return;
} else if (e.key == 'Enter' && popup.selected) { // 「選択中」設定の要素がある且つEnter
popup.selected.click(); // 「選択中」設定の要素をクリックしたことにする
popup.area.classList.add('hidden'); // ポップアップを非表示にする
return;
}
// 以下サジェスト機能
if (e.shiftKey || e.ctrlKey || e.altKey) return; // 修飾キー押下時は以下の処理をしない
popup.area.innerHTML = ''; // ポップアップ内をクリア
setTimeout(() => { // キー入力してからcursorPositionを取得するために一瞬遅らせる
suggestOnTextarea(hoge, '[', (value, str, cursorPosition) => { // 上記リンク参照
// value,str,cursorPositionにはsuggestOnTextarea内の処理で発生したデータがそれぞれ渡されている
// value: 本文全体
// str: "["以降に入力されている文字列
// cursorPosition: キャレットの現在位置
// 直前が"[["になっているか判定
if (value.substring(cursorPosition - str.length - 2, cursorPosition - str.length) != '[[') return;
// DATA内からtitleにstrを含むデータをfilterで絞り込む
const filter = DATA.filter(obj => obj.title.toLowerCase().includes(str.toLowerCase()));
if (!filter.length) return; // なければ終了
editDOM(popup.area, {
css: {
top: mousePos.y, // 今カーソルがある縦位置
left: mousePos.x, // 今カーソルがある横位置
}
})
for (const data of filter) {
createDOM('p', {
textContent: data.title,
title: data.body, // 本文をツールチップに表示
className: 'clickable',
onclick: () => {
// ""以前の部分に、選択したデータのタイトルと""を足し、キャレット位置以降の部分をくっつけて本文全体を作る
hoge.value = value.substring(0, cursorPosition - str.length) + data.title + ']]' + value.substring(cursorPosition);
// キャレット位置を"]]"の後にする
hoge.selectionEnd = cursorPosition - str.length + data.title.length + 2;
}
}, popup)
}
})
}, 10)
})
code:css
position: fixed;
width: 200px;
max-height: 200px;
overflow-y: auto;
border-radius: 5px;
font-size: 12px;
}
border-bottom: #888 1px solid; padding: 2px;
}
background-color: coral;
}
.clickable {
cursor: pointer;
}
.clickable:hover {
transition: background-color 0.3s;
}
.hidden {
display: none;
}