Passed
Push — master ( 5f67fe...8bce0b )
by Jesús
02:16
created

src/modules/wordInput/infrastructure/wordInput.ts   A

Complexity

Total Complexity 33
Complexity/F 1.94

Size

Lines of Code 245
Function Count 17

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 196
dl 0
loc 245
rs 9.76
c 0
b 0
f 0
wmc 33
mnd 16
bc 16
fnc 17
bpm 0.9411
cpm 1.9411
noi 0

17 Functions

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