132 lines
4.4 KiB
JavaScript
132 lines
4.4 KiB
JavaScript
import { state } from './state.js';
|
|
|
|
const el = id => document.getElementById(id);
|
|
|
|
// Emoji tab completion via emoji-mart SearchIndex.
|
|
// Lazily initialised on first use so it doesn't block startup.
|
|
let emojiSearchReady = false;
|
|
async function initEmojiSearch() {
|
|
if (emojiSearchReady) return;
|
|
if (!window.EmojiMart?.SearchIndex) return;
|
|
try {
|
|
const res = await fetch('vendor/emoji-data.json');
|
|
const data = await res.json();
|
|
await EmojiMart.init({ data });
|
|
emojiSearchReady = true;
|
|
} catch (e) {
|
|
console.warn('Emoji search init failed:', e);
|
|
}
|
|
}
|
|
|
|
async function searchEmoji(stem) {
|
|
await initEmojiSearch();
|
|
if (!emojiSearchReady) return [];
|
|
const results = await EmojiMart.SearchIndex.search(stem);
|
|
return (results || []).map(e => e.skins[0].native + e.id);
|
|
}
|
|
|
|
const hist = { lines: [], pos: -1, draft: '' };
|
|
const tab = { matches: [], pos: -1, stem: '' };
|
|
|
|
export function sendInput(wsSend) {
|
|
const buf = state.buffers.get(state.activeBufferId);
|
|
const text = el('chat-input').value.trim();
|
|
if (!buf || !text) return;
|
|
hist.lines.unshift(text);
|
|
hist.pos = -1;
|
|
tab.matches = [];
|
|
tab.pos = -1;
|
|
wsSend({ request: 'POST /api/input', body: { buffer_name: buf.name, command: text } });
|
|
el('chat-input').value = '';
|
|
}
|
|
|
|
export function onInputKey(e, wsSend) {
|
|
if (e.key === 'Tab') {
|
|
e.preventDefault();
|
|
doTabComplete();
|
|
return;
|
|
}
|
|
if (e.key !== 'Shift') { tab.matches = []; tab.pos = -1; }
|
|
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendInput(wsSend);
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
if (hist.pos === -1) hist.draft = el('chat-input').value;
|
|
hist.pos = Math.min(hist.pos + 1, hist.lines.length - 1);
|
|
if (hist.lines[hist.pos] !== undefined) el('chat-input').value = hist.lines[hist.pos];
|
|
} else if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
hist.pos = Math.max(hist.pos - 1, -1);
|
|
el('chat-input').value = hist.pos === -1 ? hist.draft : hist.lines[hist.pos];
|
|
}
|
|
}
|
|
|
|
function doTabComplete() {
|
|
const input = el('chat-input');
|
|
const val = input.value;
|
|
const caret = input.selectionStart;
|
|
const before = val.slice(0, caret);
|
|
const tokenMatch = before.match(/(\S+)$/);
|
|
const token = tokenMatch ? tokenMatch[1] : '';
|
|
if (!token) return;
|
|
|
|
const lower = token.toLowerCase();
|
|
|
|
if (tab.matches.length === 0 || tab.stem !== lower) {
|
|
const buf = state.buffers.get(state.activeBufferId);
|
|
if (!buf) return;
|
|
|
|
const prevWord = before.slice(0, before.length - token.length).trim().split(/\s+/).pop() || '';
|
|
const wantChannel = token.startsWith('#') || prevWord.toLowerCase() === '/join';
|
|
const wantEmoji = token.startsWith(':') && token.length > 1;
|
|
|
|
if (wantEmoji) {
|
|
const stem = token.slice(1).replace(/:$/, '').toLowerCase();
|
|
// Async: kick off search and return; next Tab press will use the results
|
|
searchEmoji(stem).then(results => {
|
|
tab.matches = results;
|
|
tab.stem = lower;
|
|
tab.pos = -1;
|
|
if (results.length > 0) doComplete(input, val, caret, before, token);
|
|
});
|
|
return; // first Tab fetches; subsequent Tab presses cycle
|
|
}
|
|
|
|
let candidates;
|
|
if (wantChannel) {
|
|
candidates = [...state.buffers.values()]
|
|
.filter(b => {
|
|
const lv = b.local_variables || {};
|
|
return lv.type === 'channel' || (b.short_name || '').startsWith('#');
|
|
})
|
|
.map(b => b.short_name || b.name);
|
|
} else {
|
|
candidates = Object.values(buf.nicks || {}).map(n => n.name);
|
|
}
|
|
|
|
tab.matches = candidates.filter(c => c.toLowerCase().startsWith(lower));
|
|
tab.matches.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
|
|
tab.pos = -1;
|
|
tab.stem = lower;
|
|
if (tab.matches.length === 0) return;
|
|
}
|
|
|
|
doComplete(input, val, caret, before, token);
|
|
}
|
|
|
|
function doComplete(input, val, caret, before, token) {
|
|
if (tab.matches.length === 0) return;
|
|
tab.pos = (tab.pos + 1) % tab.matches.length;
|
|
const match = tab.matches[tab.pos];
|
|
const isEmoji = /^\p{Emoji}/u.test(match);
|
|
const insert = isEmoji ? [...match][0] : match;
|
|
const atStart = before.trimStart() === token;
|
|
const isNick = !insert.startsWith('#') && !isEmoji;
|
|
const suffix = (atStart && isNick) ? ': ' : ' ';
|
|
const completed = before.slice(0, before.length - token.length) + insert + suffix;
|
|
input.value = completed + val.slice(caret);
|
|
input.selectionStart = input.selectionEnd = completed.length;
|
|
}
|