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

src/modules/wordInput/infrastructure/wordInput.ts   B

Complexity

Total Complexity 44
Complexity/F 2

Size

Lines of Code 285
Function Count 22

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 232
dl 0
loc 285
rs 8.8798
c 0
b 0
f 0
wmc 44
mnd 22
bc 22
fnc 22
bpm 1
cpm 2
noi 0

22 Functions

Rating   Name   Duplication   Size   Complexity  
A wordInput.ts ➔ handleWordInputBlur 0 4 1
A wordInput.ts ➔ handleClearInput 0 9 1
A wordInput.ts ➔ setupWordInput 0 18 3
A wordInput.ts ➔ updateSuggestionSelection 0 8 2
A wordInput.ts ➔ handleValidWord 0 7 2
A wordInput.ts ➔ toggleClearButton 0 10 3
A wordInput.ts ➔ handleInvalidWord 0 7 1
A wordInput.ts ➔ validateWordInput 0 15 3
A wordInput.ts ➔ handleArrowDown 0 4 1
A wordInput.ts ➔ clearSuggestionSelection 0 4 1
A wordInput.ts ➔ handleKeydown 0 27 3
A wordInput.ts ➔ handleWordInput 0 28 4
A wordInput.ts ➔ clearWordInput 0 6 1
A wordInput.ts ➔ scheduleErrorAutoClear 0 8 2
A wordInput.ts ➔ syncWordInputFromState 0 15 5
A wordInput.ts ➔ clearInputError 0 3 1
A wordInput.ts ➔ hideSuggestions 0 11 2
A wordInput.ts ➔ handleEnterKey 0 6 3
A wordInput.ts ➔ handleArrowUp 0 4 1
A wordInput.ts ➔ showSuggestions 0 31 1
A wordInput.ts ➔ selectWord 0 12 2
A wordInput.ts ➔ handleEscapeKey 0 4 1

How to fix   Complexity   

Complexity

Complex classes like src/modules/wordInput/infrastructure/wordInput.ts often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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