Passed
Push — master ( c6e1c4...17fae3 )
by Jesús
02:10
created

wordInput.ts ➔ handleValidWord   A

Complexity

Conditions 2

Size

Total Lines 7
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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