Passed
Push — master ( 09f8ce...cd7a74 )
by Jesús
02:16
created

wordInput.ts ➔ showInvalidWordToast   B

Complexity

Conditions 5

Size

Total Lines 33
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 26
dl 0
loc 33
rs 8.7893
c 0
b 0
f 0
1
import { elements } from '../../bip39';
2
import { state, setStateFromIndex, resetBoxes } from '../../bip39';
3
import { updateDisplay, setSyncWordInputCallback } from '../../display';
4
import { currentTranslations } from '../../language';
5
import { isWordInWordlist, getWordIndex, binaryValueToIndex, getWordByIndex } from '../domain/wordInputHelpers';
6
7
let selectedSuggestionIndex = -1;
8
let hideSuggestionsTimeout: NodeJS.Timeout | null = null;
9
let invalidToastShowTimeout: NodeJS.Timeout | null = null;
10
let invalidToastHideTimeout: NodeJS.Timeout | null = null;
11
let invalidToastRemoveTimeout: NodeJS.Timeout | null = null;
12
13
export function setupWordInput(): void {
14
  // Register callback to avoid circular dependency
15
  setSyncWordInputCallback(syncWordInputFromState);
16
17
  elements.wordInput.addEventListener('input', handleWordInput);
18
  elements.wordInput.addEventListener('keydown', handleKeydown);
19
  elements.wordInput.addEventListener('focus', handleWordInput);
20
  elements.wordInput.addEventListener('blur', handleWordInputBlur);
21
22
  // Handle click outside to close suggestions
23
  document.addEventListener('click', e => {
24
    if (!elements.wordInput.contains(e.target as Node) && !elements.wordSuggestions.contains(e.target as Node)) {
25
      hideSuggestions();
26
    }
27
  });
28
}
29
30
function handleWordInputBlur(): void {
31
  hideSuggestions();
32
  validateWordInput();
33
}
34
35
function validateWordInput(): void {
36
  const value = elements.wordInput.value.trim().toLowerCase();
37
38
  if (!value) {
39
    elements.wordInput.classList.remove('error');
40
    return;
41
  }
42
43
  // Check if word exists in wordlist using helper
44
  const wordExists = isWordInWordlist(value, state.wordlist);
45
46
  if (wordExists) {
47
    elements.wordInput.classList.remove('error');
48
    return;
49
  }
50
51
  // Invalid word - show error and reset boxes
52
  elements.wordInput.classList.add('error');
53
  resetBoxes();
54
  updateDisplay();
55
  showInvalidWordToast();
56
}
57
58
function showInvalidWordToast(): void {
59
  const existingToast = document.getElementById('invalid-word-toast');
60
  if (existingToast) {
61
    existingToast.remove();
62
  }
63
64
  // Clear existing timers
65
  if (invalidToastShowTimeout) clearTimeout(invalidToastShowTimeout);
66
  if (invalidToastHideTimeout) clearTimeout(invalidToastHideTimeout);
67
  if (invalidToastRemoveTimeout) clearTimeout(invalidToastRemoveTimeout);
68
69
  const toast = document.createElement('div');
70
  toast.id = 'invalid-word-toast';
71
  toast.className = 'toast';
72
  toast.textContent = currentTranslations.invalidWordMessage;
73
  toast.setAttribute('role', 'alert');
74
  toast.setAttribute('aria-live', 'polite');
75
76
  document.body.appendChild(toast);
77
78
  // Trigger animation
79
  invalidToastShowTimeout = setTimeout(() => {
80
    toast.classList.add('show');
81
  }, 0);
82
83
  // Remove after 3 seconds
84
  invalidToastHideTimeout = setTimeout(() => {
85
    toast.classList.remove('show');
86
    invalidToastRemoveTimeout = setTimeout(() => {
87
      toast.remove();
88
    }, 300);
89
  }, 3000);
90
}
91
92
function handleWordInput(): void {
93
  const value = elements.wordInput.value.trim().toLowerCase();
94
95
  if (!value) {
96
    hideSuggestions();
97
    return;
98
  }
99
100
  // Find matching words
101
  const matches = state.wordlist.filter(word => word.toLowerCase().startsWith(value));
102
103
  if (matches.length === 0) {
104
    hideSuggestions();
105
    return;
106
  }
107
108
  // Show suggestions
109
  showSuggestions(matches.slice(0, 10)); // Limit to 10 suggestions
110
  selectedSuggestionIndex = -1;
111
}
112
113
function showSuggestions(matches: string[]): void {
114
  elements.wordSuggestions.innerHTML = '';
115
116
  matches.forEach((word, index) => {
117
    const wordIndex = state.wordlist.indexOf(word);
118
    const item = document.createElement('div');
119
    item.className = 'suggestion-item';
120
    item.setAttribute('role', 'option');
121
    item.setAttribute('data-index', index.toString());
122
123
    item.innerHTML = `
124
      <span class="suggestion-word">${word}</span>
125
      <span class="suggestion-index">#${wordIndex + 1}</span>
126
    `;
127
128
    item.addEventListener('mousedown', e => {
129
      e.preventDefault(); // Prevent blur event
130
      selectWord(word);
131
    });
132
133
    item.addEventListener('mouseenter', () => {
134
      clearSuggestionSelection();
135
      selectedSuggestionIndex = index;
136
      item.setAttribute('aria-selected', 'true');
137
    });
138
139
    elements.wordSuggestions.appendChild(item);
140
  });
141
142
  elements.wordSuggestions.removeAttribute('hidden');
143
}
144
145
function hideSuggestions(): void {
146
  // Clear existing timeout to prevent multiple timers
147
  if (hideSuggestionsTimeout) {
148
    clearTimeout(hideSuggestionsTimeout);
149
  }
150
151
  hideSuggestionsTimeout = setTimeout(() => {
152
    elements.wordSuggestions.setAttribute('hidden', '');
153
    selectedSuggestionIndex = -1;
154
  }, 200);
155
}
156
157
function handleArrowDown(suggestions: NodeListOf<Element>): void {
158
  selectedSuggestionIndex = Math.min(selectedSuggestionIndex + 1, suggestions.length - 1);
159
  updateSuggestionSelection(suggestions);
160
}
161
162
function handleArrowUp(suggestions: NodeListOf<Element>): void {
163
  selectedSuggestionIndex = Math.max(selectedSuggestionIndex - 1, 0);
164
  updateSuggestionSelection(suggestions);
165
}
166
167
function handleEnterKey(suggestions: NodeListOf<Element>): void {
168
  if (selectedSuggestionIndex >= 0) {
169
    const selectedItem = suggestions[selectedSuggestionIndex] as HTMLElement;
170
    const word = selectedItem.querySelector('.suggestion-word')?.textContent || '';
171
    selectWord(word);
172
  }
173
}
174
175
function handleEscapeKey(): void {
176
  hideSuggestions();
177
  elements.wordInput.blur();
178
}
179
180
function handleKeydown(e: KeyboardEvent): void {
181
  const suggestions = elements.wordSuggestions.querySelectorAll('.suggestion-item');
182
183
  if (suggestions.length === 0) {
184
    return;
185
  }
186
187
  switch (e.key) {
188
    case 'ArrowDown':
189
      e.preventDefault();
190
      handleArrowDown(suggestions);
191
      break;
192
193
    case 'ArrowUp':
194
      e.preventDefault();
195
      handleArrowUp(suggestions);
196
      break;
197
198
    case 'Enter':
199
      e.preventDefault();
200
      handleEnterKey(suggestions);
201
      break;
202
203
    case 'Escape':
204
      handleEscapeKey();
205
      break;
206
  }
207
}
208
209
function updateSuggestionSelection(suggestions: NodeListOf<Element>): void {
210
  clearSuggestionSelection();
211
212
  if (selectedSuggestionIndex >= 0 && selectedSuggestionIndex < suggestions.length) {
213
    const selectedItem = suggestions[selectedSuggestionIndex] as HTMLElement;
214
    selectedItem.setAttribute('aria-selected', 'true');
215
    selectedItem.scrollIntoView({ block: 'nearest' });
216
  }
217
}
218
219
function clearSuggestionSelection(): void {
220
  elements.wordSuggestions.querySelectorAll('.suggestion-item').forEach(item => {
221
    item.setAttribute('aria-selected', 'false');
222
  });
223
}
224
225
function selectWord(word: string): void {
226
  const wordIndex = getWordIndex(word, state.wordlist);
227
228
  if (wordIndex === -1) return;
229
230
  elements.wordInput.value = word;
231
  elements.wordInput.classList.remove('error');
232
  setStateFromIndex(wordIndex);
233
  updateDisplay();
234
  hideSuggestions();
235
  elements.wordInput.blur();
236
}
237
238
export function clearWordInput(): void {
239
  elements.wordInput.value = '';
240
  elements.wordInput.classList.remove('error');
241
  hideSuggestions();
242
}
243
244
export function syncWordInputFromState(): void {
245
  const index = state.boxes.reduce((acc, val, i) => acc + (val ? Math.pow(2, 11 - i) : 0), 0);
246
247
  if (index > 0 && index <= state.wordlist.length) {
248
    const wordIndex = binaryValueToIndex(index);
249
    const word = getWordByIndex(wordIndex, state.wordlist);
250
    if (word && elements.wordInput.value !== word) {
251
      elements.wordInput.value = word;
252
    }
253
  } else {
254
    if (elements.wordInput.value !== '') {
255
      elements.wordInput.value = '';
256
    }
257
  }
258
}
259