@emaxe/tuigram 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -1
- package/CHANGELOG.ru.md +65 -1
- package/README.md +63 -4
- package/README.ru.md +64 -4
- package/package.json +1 -1
- package/src/telegram/messages.js +98 -19
- package/src/ui/app.js +189 -21
- package/src/ui/components/chatList.js +64 -0
- package/src/ui/components/chatView.js +256 -35
- package/src/ui/components/header.js +25 -1
- package/src/ui/components/inputBox.js +41 -1
- package/src/ui/components/modals/actionModal.js +23 -0
- package/src/ui/components/modals/chatInfoModal.js +9 -0
- package/src/ui/components/modals/confirmModal.js +8 -0
- package/src/ui/components/modals/fileModal.js +26 -1
- package/src/ui/components/modals/filePickerModal.js +9 -0
- package/src/ui/components/modals/helpModal.js +23 -4
- package/src/ui/components/modals/imageViewerModal.js +172 -0
- package/src/ui/components/statusBar.js +54 -1
- package/src/ui/modalMouse.js +43 -0
- package/src/ui/screen.js +92 -3
- package/src/utils/image.js +8 -8
- package/src/utils/mouse.js +223 -0
package/src/ui/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import { createScreen } from "./screen.js";
|
|
3
|
+
import { createScreen, setMouseCapture } from "./screen.js";
|
|
4
4
|
import { getTheme } from "./theme.js";
|
|
5
5
|
import { createHeader } from "./components/header.js";
|
|
6
6
|
import { createChatList } from "./components/chatList.js";
|
|
@@ -12,16 +12,18 @@ import { createChatInfoModal } from "./components/modals/chatInfoModal.js";
|
|
|
12
12
|
import { createActionModal } from "./components/modals/actionModal.js";
|
|
13
13
|
import { createFileModal } from "./components/modals/fileModal.js";
|
|
14
14
|
import { createConfirmModal } from "./components/modals/confirmModal.js";
|
|
15
|
+
import { createImageViewerModal } from "./components/modals/imageViewerModal.js";
|
|
15
16
|
import { state } from "../state.js";
|
|
16
17
|
import { config } from "../config.js";
|
|
17
18
|
import { fetchDialogs } from "../telegram/dialogs.js";
|
|
18
19
|
import { setMessagePalette } from "../telegram/formatter.js";
|
|
19
|
-
import { fetchHistory, sendMessage, editMessage, deleteMessages, sendFiles, downloadMedia, sendReaction, markAsRead, loadMessageImagePreview } from "../telegram/messages.js";
|
|
20
|
+
import { fetchHistory, sendMessage, editMessage, deleteMessages, sendFiles, downloadMedia, sendReaction, markAsRead, loadMessageImagePreview, downloadImageBuffer, renderMessageThumbnail } from "../telegram/messages.js";
|
|
20
21
|
import { startTelegramListener } from "../telegram/listener.js";
|
|
21
22
|
import { logout } from "../telegram/auth.js";
|
|
22
23
|
import { ensureDir, inspectLocalFile } from "../utils/storage.js";
|
|
23
24
|
import { formatFileSize } from "../utils/time.js";
|
|
24
25
|
import { parseSendFileArgs } from "../utils/commands.js";
|
|
26
|
+
import { isRightClick } from "../utils/mouse.js";
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Запускает полноэкранный TUI-клиент Telegram.
|
|
@@ -43,8 +45,79 @@ export async function startTui(client, me) {
|
|
|
43
45
|
state.setConnectionStatus("connected");
|
|
44
46
|
|
|
45
47
|
// 1. Создание визуальных компонентов
|
|
46
|
-
const header = createHeader(screen, theme
|
|
47
|
-
|
|
48
|
+
const header = createHeader(screen, theme, {
|
|
49
|
+
onHelp: () => {
|
|
50
|
+
releaseInputs();
|
|
51
|
+
helpModal.show();
|
|
52
|
+
},
|
|
53
|
+
onChatInfo: () => {
|
|
54
|
+
if (state.activeChat) {
|
|
55
|
+
releaseInputs();
|
|
56
|
+
chatInfoModal.show(state.activeChat);
|
|
57
|
+
} else {
|
|
58
|
+
statusBar.showMessage("Сначала выберите чат слева!", "warning");
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
onStatusClick: () => {
|
|
62
|
+
const statusText = state.connectionStatus === "connected"
|
|
63
|
+
? "Подключение активно: MTProto онлайн"
|
|
64
|
+
: `Статус сети: ${state.connectionStatus}`;
|
|
65
|
+
statusBar.showMessage(statusText, "info");
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const statusBar = createStatusBar(screen, theme, {
|
|
70
|
+
onFocusNext: () => moveFocus(1),
|
|
71
|
+
onSelectOrSubmit: () => {
|
|
72
|
+
const focused = detectFocus();
|
|
73
|
+
if (focused === "input") {
|
|
74
|
+
inputBox.textarea.emit("key enter");
|
|
75
|
+
} else if (focused === "chatList") {
|
|
76
|
+
chatList.list.emit("key enter");
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
onFilterTabs: () => {
|
|
80
|
+
releaseInputs();
|
|
81
|
+
chatList.focus();
|
|
82
|
+
statusBar.showMessage("Фильтры: нажмите 1-6 для выбора вкладки", "info");
|
|
83
|
+
},
|
|
84
|
+
onSearch: () => {
|
|
85
|
+
chatList.searchBox.setValue("");
|
|
86
|
+
chatList.searchBox.focus();
|
|
87
|
+
screen.render();
|
|
88
|
+
},
|
|
89
|
+
onHelp: () => {
|
|
90
|
+
releaseInputs();
|
|
91
|
+
helpModal.show();
|
|
92
|
+
},
|
|
93
|
+
onAction: () => {
|
|
94
|
+
if (!state.activeChat) {
|
|
95
|
+
statusBar.showMessage("Сначала выберите чат слева!", "warning");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const msg = chatView.getTargetMessage();
|
|
99
|
+
if (msg) {
|
|
100
|
+
releaseInputs();
|
|
101
|
+
actionModal.show(msg);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
onChatInfo: () => {
|
|
105
|
+
if (state.activeChat) {
|
|
106
|
+
releaseInputs();
|
|
107
|
+
chatInfoModal.show(state.activeChat);
|
|
108
|
+
} else {
|
|
109
|
+
statusBar.showMessage("Сначала выберите чат слева!", "warning");
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
onQuit: () => {
|
|
113
|
+
releaseInputs();
|
|
114
|
+
confirmModal.ask("Выйти из TuiGram?", () => {
|
|
115
|
+
listener?.stop();
|
|
116
|
+
screen.destroy();
|
|
117
|
+
process.exit(0);
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
});
|
|
48
121
|
|
|
49
122
|
// Модальные окна
|
|
50
123
|
const helpModal = createHelpModal(screen, theme);
|
|
@@ -54,6 +127,16 @@ export async function startTui(client, me) {
|
|
|
54
127
|
onSendFile: (files, options) => sendFilesToActiveChat(files, options),
|
|
55
128
|
});
|
|
56
129
|
|
|
130
|
+
const imageViewerModal = createImageViewerModal(screen, theme, {
|
|
131
|
+
onLoadFullImage: (msg) => downloadImageBuffer(client, msg.rawMessage),
|
|
132
|
+
// Пока качается оригинал, показываем встроенную в сообщение миниатюру
|
|
133
|
+
onRenderPlaceholder: (msg, size) => renderMessageThumbnail(msg.rawMessage, {
|
|
134
|
+
maxWidth: size.maxWidth,
|
|
135
|
+
maxHeight: size.maxHeight,
|
|
136
|
+
useCache: false,
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
|
|
57
140
|
const actionModal = createActionModal(screen, theme, {
|
|
58
141
|
onAction: async (actionId, msg) => {
|
|
59
142
|
if (!state.activeChat) return;
|
|
@@ -168,6 +251,19 @@ export async function startTui(client, me) {
|
|
|
168
251
|
releaseInputs();
|
|
169
252
|
actionModal.show(msg);
|
|
170
253
|
},
|
|
254
|
+
onSelectMessage: (msg) => {
|
|
255
|
+
const preview = (msg.text || msg.mediaDescription || "вложение").replace(/\s+/g, " ").slice(0, 30);
|
|
256
|
+
statusBar.showMessage(
|
|
257
|
+
`Выделено #${msg.id}: "${preview}" · [Enter] или правый клик — действия`,
|
|
258
|
+
"info",
|
|
259
|
+
3000
|
|
260
|
+
);
|
|
261
|
+
},
|
|
262
|
+
onOpenImage: (msg) => {
|
|
263
|
+
releaseInputs();
|
|
264
|
+
imageViewerModal.show(msg);
|
|
265
|
+
},
|
|
266
|
+
onFocusRequest: () => releaseInputs(),
|
|
171
267
|
});
|
|
172
268
|
|
|
173
269
|
// 4. Поле ввода (нижняя панель)
|
|
@@ -205,6 +301,8 @@ export async function startTui(client, me) {
|
|
|
205
301
|
onCancelContext: () => {
|
|
206
302
|
statusBar.showMessage("Режим ответа/редактирования сброшен", "info", 2000);
|
|
207
303
|
},
|
|
304
|
+
onReplyLast: () => startReply(),
|
|
305
|
+
onEditLast: () => startEdit(),
|
|
208
306
|
onSlashCommand: (cmd, args) => {
|
|
209
307
|
switch (cmd) {
|
|
210
308
|
case "help":
|
|
@@ -269,6 +367,36 @@ export async function startTui(client, me) {
|
|
|
269
367
|
},
|
|
270
368
|
});
|
|
271
369
|
|
|
370
|
+
// Переключение фокуса по клику мышью на любую из трех панелей
|
|
371
|
+
chatList.container.on("click", (data) => {
|
|
372
|
+
if (isRightClick(data)) return;
|
|
373
|
+
if (screen.focused !== chatList.list && screen.focused !== chatList.searchBox) {
|
|
374
|
+
releaseInputs();
|
|
375
|
+
chatList.focus();
|
|
376
|
+
statusBar.showMessage("Фокус: список чатов", "info", 2000);
|
|
377
|
+
screen.render();
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
chatView.container.on("click", (data) => {
|
|
382
|
+
if (isRightClick(data)) return;
|
|
383
|
+
if (screen.focused !== chatView.scrollBox) {
|
|
384
|
+
releaseInputs();
|
|
385
|
+
chatView.focus();
|
|
386
|
+
statusBar.showMessage("Фокус: лента сообщений", "info", 2000);
|
|
387
|
+
screen.render();
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
inputBox.container.on("click", (data) => {
|
|
392
|
+
if (isRightClick(data)) return;
|
|
393
|
+
if (screen.focused !== inputBox.textarea) {
|
|
394
|
+
inputBox.focus();
|
|
395
|
+
statusBar.showMessage("Фокус: поле ввода", "info", 2000);
|
|
396
|
+
screen.render();
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
272
400
|
// 5. Реакция на события состояния
|
|
273
401
|
state.on("dialogs_updated", (dialogs) => {
|
|
274
402
|
chatList.setDialogs(dialogs);
|
|
@@ -290,6 +418,7 @@ export async function startTui(client, me) {
|
|
|
290
418
|
typingUser: state.getTypingUser(chat?.id),
|
|
291
419
|
});
|
|
292
420
|
const msgs = chat ? state.getMessages(chat.id) : [];
|
|
421
|
+
chatView.setSelected(null);
|
|
293
422
|
chatView.setMessages(msgs);
|
|
294
423
|
});
|
|
295
424
|
|
|
@@ -459,6 +588,31 @@ export async function startTui(client, me) {
|
|
|
459
588
|
chatList.release?.();
|
|
460
589
|
}
|
|
461
590
|
|
|
591
|
+
/** Включает режим ответа на выделенное сообщение, иначе — на последнее в ленте. */
|
|
592
|
+
function startReply() {
|
|
593
|
+
if (!state.activeChat) return;
|
|
594
|
+
const target = chatView.getTargetMessage();
|
|
595
|
+
if (target) {
|
|
596
|
+
inputBox.setContext("reply", target);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** Включает правку выделенного своего сообщения, иначе — последнего своего. */
|
|
601
|
+
function startEdit() {
|
|
602
|
+
if (!state.activeChat) return;
|
|
603
|
+
const selected = chatView.getSelected();
|
|
604
|
+
if (selected?.out) {
|
|
605
|
+
inputBox.setContext("edit", selected);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const ownMsgs = state.getMessages(state.activeChat.id).filter((m) => m.out);
|
|
609
|
+
if (ownMsgs.length > 0) {
|
|
610
|
+
inputBox.setContext("edit", ownMsgs[ownMsgs.length - 1]);
|
|
611
|
+
} else {
|
|
612
|
+
statusBar.showMessage("В этом чате нет ваших сообщений для правки", "warning", 3000);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
462
616
|
// 7. Глобальные сочетания клавиш
|
|
463
617
|
// Цикл фокуса: список чатов -> лента сообщений -> поле ввода -> список чатов.
|
|
464
618
|
// Без ленты в цикле были недостижимы прокрутка, подгрузка истории и меню действий.
|
|
@@ -501,17 +655,18 @@ export async function startTui(client, me) {
|
|
|
501
655
|
screen.key(["tab"], () => moveFocus(1));
|
|
502
656
|
screen.key(["S-tab"], () => moveFocus(-1));
|
|
503
657
|
|
|
658
|
+
// Когда лента в фокусе, эти клавиши обрабатывает она сама — иначе прокрутка удваивается
|
|
504
659
|
screen.key(["pageup", "C-u"], () => {
|
|
505
|
-
if (!state.activeChat) return;
|
|
660
|
+
if (!state.activeChat || screen.focused === chatView.scrollBox) return;
|
|
506
661
|
chatView.scrollBox.scroll(-10);
|
|
507
|
-
if (chatView.scrollBox.
|
|
662
|
+
if ((chatView.scrollBox.childBase || 0) <= 0) {
|
|
508
663
|
chatView.loadMore?.();
|
|
509
664
|
}
|
|
510
665
|
screen.render();
|
|
511
666
|
});
|
|
512
667
|
|
|
513
668
|
screen.key(["pagedown", "C-d"], () => {
|
|
514
|
-
if (!state.activeChat) return;
|
|
669
|
+
if (!state.activeChat || screen.focused === chatView.scrollBox) return;
|
|
515
670
|
chatView.scrollBox.scroll(10);
|
|
516
671
|
screen.render();
|
|
517
672
|
});
|
|
@@ -540,23 +695,36 @@ export async function startTui(client, me) {
|
|
|
540
695
|
}
|
|
541
696
|
});
|
|
542
697
|
|
|
543
|
-
screen.key(["C-r"], () =>
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
698
|
+
screen.key(["C-r"], () => startReply());
|
|
699
|
+
|
|
700
|
+
screen.key(["C-e"], () => startEdit());
|
|
701
|
+
|
|
702
|
+
// Меню действий над выделенным сообщением из любой панели.
|
|
703
|
+
// Когда лента в фокусе, Ctrl+A обрабатывает она сама.
|
|
704
|
+
screen.key(["C-a"], () => {
|
|
705
|
+
if (screen.focused === chatView.scrollBox) return;
|
|
706
|
+
if (!state.activeChat) {
|
|
707
|
+
statusBar.showMessage("Сначала выберите чат слева!", "warning");
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const msg = chatView.getTargetMessage();
|
|
711
|
+
if (msg) {
|
|
712
|
+
releaseInputs();
|
|
713
|
+
actionModal.show(msg);
|
|
549
714
|
}
|
|
550
715
|
});
|
|
551
716
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
717
|
+
// Пока мышь захвачена приложением, терминал не даёт выделять текст для копирования
|
|
718
|
+
screen.key(["f12"], () => {
|
|
719
|
+
const enabled = !screen.mouseCaptured;
|
|
720
|
+
setMouseCapture(screen, enabled);
|
|
721
|
+
statusBar.showMessage(
|
|
722
|
+
enabled
|
|
723
|
+
? "Мышь снова управляет интерфейсом"
|
|
724
|
+
: "Мышь отдана терминалу — можно выделять и копировать текст. [F12] вернуть",
|
|
725
|
+
enabled ? "info" : "warning",
|
|
726
|
+
5000
|
|
727
|
+
);
|
|
560
728
|
});
|
|
561
729
|
|
|
562
730
|
screen.key(["C-q"], () => {
|
|
@@ -4,6 +4,8 @@ import { escapeBlessed } from "../../telegram/formatter.js";
|
|
|
4
4
|
import { fg, badge } from "../theme.js";
|
|
5
5
|
import unicode from "neo-blessed/lib/unicode.js";
|
|
6
6
|
|
|
7
|
+
import { getTabByCoordinate, isRightClick } from "../../utils/mouse.js";
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Создаёт компонент списка диалогов (левая панель).
|
|
9
11
|
* @param {blessed.Widgets.Screen} screen
|
|
@@ -20,6 +22,7 @@ export function createChatList(screen, theme, { onSelectDialog, onTabChange, onS
|
|
|
20
22
|
left: 0,
|
|
21
23
|
width: "35%",
|
|
22
24
|
bottom: 1,
|
|
25
|
+
mouse: true,
|
|
23
26
|
border: {
|
|
24
27
|
type: "line",
|
|
25
28
|
},
|
|
@@ -40,6 +43,8 @@ export function createChatList(screen, theme, { onSelectDialog, onTabChange, onS
|
|
|
40
43
|
right: 0,
|
|
41
44
|
height: 1,
|
|
42
45
|
tags: true,
|
|
46
|
+
mouse: true,
|
|
47
|
+
clickable: true,
|
|
43
48
|
style: {
|
|
44
49
|
bg: theme.tabs.bg,
|
|
45
50
|
fg: theme.tabs.fg,
|
|
@@ -64,6 +69,18 @@ export function createChatList(screen, theme, { onSelectDialog, onTabChange, onS
|
|
|
64
69
|
tabsBox.setContent(rendered);
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
tabsBox.on("click", (data) => {
|
|
73
|
+
if (isRightClick(data)) return;
|
|
74
|
+
const relX = data.x - (tabsBox.aleft || 0);
|
|
75
|
+
const tabKey = getTabByCoordinate(relX, TAB_KEYS, TAB_NAMES);
|
|
76
|
+
if (tabKey) {
|
|
77
|
+
currentTab = tabKey;
|
|
78
|
+
renderTabs();
|
|
79
|
+
onTabChange?.(currentTab);
|
|
80
|
+
screen.render();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
67
84
|
// 2. Строка поиска / фильтра
|
|
68
85
|
const searchBox = blessed.textbox({
|
|
69
86
|
parent: container,
|
|
@@ -72,6 +89,7 @@ export function createChatList(screen, theme, { onSelectDialog, onTabChange, onS
|
|
|
72
89
|
right: 0,
|
|
73
90
|
height: 1,
|
|
74
91
|
inputOnFocus: true,
|
|
92
|
+
mouse: true,
|
|
75
93
|
style: {
|
|
76
94
|
bg: theme.search.bg,
|
|
77
95
|
fg: theme.search.fg,
|
|
@@ -80,6 +98,14 @@ export function createChatList(screen, theme, { onSelectDialog, onTabChange, onS
|
|
|
80
98
|
const SEARCH_PLACEHOLDER = "[/] Поиск чатов...";
|
|
81
99
|
searchBox.setValue(SEARCH_PLACEHOLDER);
|
|
82
100
|
|
|
101
|
+
searchBox.on("click", () => {
|
|
102
|
+
if (searchBox.getValue() === SEARCH_PLACEHOLDER) {
|
|
103
|
+
searchBox.setValue("");
|
|
104
|
+
}
|
|
105
|
+
searchBox.focus();
|
|
106
|
+
screen.render();
|
|
107
|
+
});
|
|
108
|
+
|
|
83
109
|
// 3. Список диалогов
|
|
84
110
|
const list = blessed.list({
|
|
85
111
|
parent: container,
|
|
@@ -118,13 +144,32 @@ export function createChatList(screen, theme, { onSelectDialog, onTabChange, onS
|
|
|
118
144
|
// не влезающий хвост на вторую строку, которой в элементе высотой 1 просто
|
|
119
145
|
// нет: хвост исчезал, а начатый перед разрывом фон бейджа непрочитанных
|
|
120
146
|
// оставался залитым до края панели. С wrap: false лишнее просто обрезается.
|
|
147
|
+
// По клику мыши сразу выбираем и открываем диалог.
|
|
121
148
|
const baseCreateItem = list.createItem.bind(list);
|
|
122
149
|
list.createItem = (content) => {
|
|
123
150
|
const item = baseCreateItem(content);
|
|
124
151
|
item.wrap = false;
|
|
152
|
+
item.on("click", (data) => {
|
|
153
|
+
if (isRightClick(data)) return;
|
|
154
|
+
const index = list.getItemIndex(item);
|
|
155
|
+
if (index !== -1 && currentDialogs[index]) {
|
|
156
|
+
list.select(index);
|
|
157
|
+
onSelectDialog?.(currentDialogs[index]);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
125
160
|
return item;
|
|
126
161
|
};
|
|
127
162
|
|
|
163
|
+
container.on("wheelup", () => {
|
|
164
|
+
list.select(list.selected - 2);
|
|
165
|
+
screen.render();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
container.on("wheeldown", () => {
|
|
169
|
+
list.select(list.selected + 2);
|
|
170
|
+
screen.render();
|
|
171
|
+
});
|
|
172
|
+
|
|
128
173
|
let currentDialogs = [];
|
|
129
174
|
|
|
130
175
|
/**
|
|
@@ -275,6 +320,25 @@ export function createChatList(screen, theme, { onSelectDialog, onTabChange, onS
|
|
|
275
320
|
|
|
276
321
|
renderTabs();
|
|
277
322
|
|
|
323
|
+
/** Подсвечивает рамку, когда список диалогов или строка поиска в фокусе. */
|
|
324
|
+
function setFocusHighlight(active) {
|
|
325
|
+
container.style.border.fg = active ? theme.borders.focusFg : theme.borders.fg;
|
|
326
|
+
screen.render();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
list.on("focus", () => setFocusHighlight(true));
|
|
330
|
+
list.on("blur", (newTarget) => {
|
|
331
|
+
if (newTarget !== searchBox && screen.focused !== searchBox) {
|
|
332
|
+
setFocusHighlight(false);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
searchBox.on("focus", () => setFocusHighlight(true));
|
|
336
|
+
searchBox.on("blur", (newTarget) => {
|
|
337
|
+
if (newTarget !== list && screen.focused !== list) {
|
|
338
|
+
setFocusHighlight(false);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
278
342
|
return {
|
|
279
343
|
container,
|
|
280
344
|
list,
|