suggest-box
重要なお知らせ (2021/8/2)
この UserScript はもうメンテナンスされていません。UserScript のコードは パブリック・ドメイン・マーク 1.0 として配布しておりますので、もしお使いになりたい方がいましたら、ライセンスの範囲内でご自由にお使い下さい。
hr.icon
ユーザー入力で絞り込み可能なアイテムリストを表示するUserScript向けのライブラリ.
https://gyazo.com/ceeecc6d5a1201baad9b14dba7aade11
導入方法
code:install.js
import { SuggestBox } from '/api/code/mizdra/suggest-box/script.js';
使い方
code:usage.js
import { SuggestBox } from '/api/code/mizdra/suggest-box/script.js';
const items = [
'foo',
'bar',
'baz',
'foobar',
'foobarbaz',
].map(itemValue => ({
value: itemValue,
node: document.createTextNode(itemValue),
}));
const suggestBox = new SuggestBox();
suggestBox.addEventListener('itementer', (e) => {
suggestBox.hide();
document.querySelector('#text-input').focus();
document.execCommand('insertText', null, e.detail.value);
});
suggestBox.addEventListener('blur', (e) => {
suggestBox.hide();
document.querySelector('#text-input').focus();
});
// ctrl+Spaceで SuggestBox を表示
document.addEventListener('keydown', (e) => {
const isCtrlSpace = e.key === ' ' && e.ctrlKey && !e.shiftKey && !e.altKey;
const isTab = e.key === 'Tab' && !e.ctrlKey && !e.shiftKey && !e.altKey;
const isShiftTab = e.key === 'Tab' && !e.ctrlKey && e.shiftKey && !e.altKey;
const isEnter = e.key === 'Enter' && !e.ctrlKey && !e.shiftKey && !e.altKey;
const isEscape = e.key === 'Escape' && !e.ctrlKey && !e.shiftKey && !e.altKey;
// IMEによる変換中は何もしない
if (e.isComposing) return;
if (suggestBox.isHidden()) {
if (isCtrlSpace) {
e.preventDefault();
e.stopPropagation();
suggestBox.show(items);
suggestBox.focus();
}
} else {
if (isTab || isShiftTab || isEnter || isEscape) {
e.preventDefault();
e.stopPropagation();
}
if (isTab) suggestBox.selectNextItem();
if (isShiftTab) suggestBox.selectPrevItem();
if (isEnter) suggestBox.enterItem();
if (isEscape) {
suggestBox.hide();
document.querySelector('#text-input').focus();
}
}
}, true);
ソースコード
code:lib.js
export function getCursor() {
const cursorNode = document.querySelector('.cursor');
return { top: cursorNode.style.top, left: cursorNode.style.left };
}
function createElementFromHTML(html) {
const el = document.createElement('div');
el.innerHTML = html;
return el.firstElementChild;
}
export function addStyleSheet() {
// cssを挿入
const style = createElementFromHTML(`
<style>
.input-box {
position: absolute;
}
</style>
`);
document.head.appendChild(style);
}
code:script.js
import { PopupMenu } from '../popup-menu/script.js';
import { getCursor, addStyleSheet } from './lib.js';
addStyleSheet();
class InputBox extends EventTarget {
constructor() {
super();
this.input = document.createElement('input');
this.input.classList.add('input-box');
this.input.style.display = 'none';
document.querySelector('.editor').appendChild(this.input);
this.input.addEventListener('input', (e) => {
this.dispatchEvent(new CustomEvent('input', { detail: e.target.value }));
});
this.input.addEventListener('blur', (e) => {
this.dispatchEvent(new CustomEvent('blur'));
});
}
show() {
const cursor = getCursor();
this.input.style.display = 'block';
this.input.style.top = cursor.top;
this.input.style.left = cursor.left;
}
hide() {
this.input.style.display = 'none';
this.input.value = ''; // clear input
}
focus() {
this.input.focus();
}
}
export class SuggestBox extends EventTarget {
constructor(options) {
super();
this.popupMenu = new PopupMenu(options.popupMenu);
this.inputBox = new InputBox();
this.items = null;
this.popupMenu.addEventListener('itementer', (e) => this._onItemEnter(e));
this.inputBox.addEventListener('input', (e) => this._onInput(e));
this.inputBox.addEventListener('blur', () => this._onBlur());
}
isHidden() {
return this.popupMenu.isHidden();
}
show(items, options) {
this.items = items;
this.popupMenu.show(items, options);
this.inputBox.show();
}
hide() {
this.popupMenu.hide();
this.inputBox.hide();
}
focus() {
this.inputBox.focus();
}
selectNextItem() {
this.popupMenu.selectNextItem();
}
selectPrevItem() {
this.popupMenu.selectPrevItem();
}
enterItem() {
this.popupMenu.enterItem();
}
_onItemEnter(e) {
this.dispatchEvent(new CustomEvent('itementer', { detail: e.detail }));
}
_onInput(e) {
const newItems = this.items.filter(item => {
const target = item.node.textContent.toLowerCase();
const input = e.detail.toLowerCase(); // ユーザ入力
return target.includes(input);
});
this.popupMenu.updateItems(newItems);
}
_onBlur() {
this.dispatchEvent(new CustomEvent('blur'));
}
}
ソースコードのライセンス