Completed
Push — master ( c8f34f...a578a1 )
by Jesús
12s
created

wordInput.ts ➔ handleClearInput   A

Complexity

Conditions 1

Size

Total Lines 9
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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