Passed
Push — master ( cb41c7...97cf22 )
by Jesús
01:56
created

wordInput.ts ➔ handleEscapeKey   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
1
import { elements } from '../core/dom';
2
import { state, setStateFromIndex, resetBoxes } from '../core/state';
3
import { updateDisplay } from './display';
4
import { currentTranslations } from '../services/language';
5
6
let selectedSuggestionIndex = -1;
7
8
export function setupWordInput(): void {
9
  elements.wordInput.addEventListener('input', handleWordInput);
10
  elements.wordInput.addEventListener('keydown', handleKeydown);
11
  elements.wordInput.addEventListener('focus', handleWordInput);
12
  elements.wordInput.addEventListener('blur', handleWordInputBlur);
13
  
14
  // Handle click outside to close suggestions
15
  document.addEventListener('click', (e) => {
16
    if (!elements.wordInput.contains(e.target as Node) && 
17
        !elements.wordSuggestions.contains(e.target as Node)) {
18
      hideSuggestions();
19
    }
20
  });
21
}
22
23
function handleWordInputBlur(): void {
24
  hideSuggestions();
25
  validateWordInput();
26
}
27
28
function validateWordInput(): void {
29
  const value = elements.wordInput.value.trim().toLowerCase();
30
  
31
  if (!value) {
32
    elements.wordInput.classList.remove('error');
33
    return;
34
  }
35
  
36
  // Check if word exists in wordlist
37
  const wordExists = state.wordlist.some(word => word.toLowerCase() === value);
38
  
39
  if (wordExists) {
40
    elements.wordInput.classList.remove('error');
41
    return;
42
  }
43
44
  // Invalid word - show error and reset boxes
45
  elements.wordInput.classList.add('error');
46
  resetBoxes();
47
  updateDisplay();
48
  showInvalidWordToast();
49
}
50
51
function showInvalidWordToast(): void {
52
  const existingToast = document.getElementById('invalid-word-toast');
53
  if (existingToast) {
54
    existingToast.remove();
55
  }
56
57
  const toast = document.createElement('div');
58
  toast.id = 'invalid-word-toast';
59
  toast.className = 'toast';
60
  toast.textContent = currentTranslations.invalidWordMessage;
61
  toast.setAttribute('role', 'alert');
62
  toast.setAttribute('aria-live', 'polite');
63
64
  document.body.appendChild(toast);
65
66
  // Trigger animation
67
  setTimeout(() => {
68
    toast.classList.add('show');
69
  }, 0);
70
71
  // Remove after 3 seconds
72
  setTimeout(() => {
73
    toast.classList.remove('show');
74
    setTimeout(() => {
75
      toast.remove();
76
    }, 300);
77
  }, 3000);
78
}
79
80
function handleWordInput(): void {
81
  const value = elements.wordInput.value.trim().toLowerCase();
82
  
83
  if (!value) {
84
    hideSuggestions();
85
    return;
86
  }
87
  
88
  // Find matching words
89
  const matches = state.wordlist.filter(word => 
90
    word.toLowerCase().startsWith(value)
91
  );
92
  
93
  if (matches.length === 0) {
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
  setTimeout(() => {
137
    elements.wordSuggestions.setAttribute('hidden', '');
138
    selectedSuggestionIndex = -1;
139
  }, 200);
140
}
141
142
function handleArrowDown(suggestions: NodeListOf<Element>): void {
143
  selectedSuggestionIndex = Math.min(selectedSuggestionIndex + 1, suggestions.length - 1);
144
  updateSuggestionSelection(suggestions);
145
}
146
147
function handleArrowUp(suggestions: NodeListOf<Element>): void {
148
  selectedSuggestionIndex = Math.max(selectedSuggestionIndex - 1, 0);
149
  updateSuggestionSelection(suggestions);
150
}
151
152
function handleEnterKey(suggestions: NodeListOf<Element>): void {
153
  if (selectedSuggestionIndex >= 0) {
154
    const selectedItem = suggestions[selectedSuggestionIndex] as HTMLElement;
155
    const word = selectedItem.querySelector('.suggestion-word')?.textContent || '';
156
    selectWord(word);
157
  }
158
}
159
160
function handleEscapeKey(): void {
161
  hideSuggestions();
162
  elements.wordInput.blur();
163
}
164
165
function handleKeydown(e: KeyboardEvent): void {
166
  const suggestions = elements.wordSuggestions.querySelectorAll('.suggestion-item');
167
  
168
  if (suggestions.length === 0) {
169
    return;
170
  }
171
  
172
  switch (e.key) {
173
    case 'ArrowDown':
174
      e.preventDefault();
175
      handleArrowDown(suggestions);
176
      break;
177
      
178
    case 'ArrowUp':
179
      e.preventDefault();
180
      handleArrowUp(suggestions);
181
      break;
182
      
183
    case 'Enter':
184
      e.preventDefault();
185
      handleEnterKey(suggestions);
186
      break;
187
      
188
    case 'Escape':
189
      handleEscapeKey();
190
      break;
191
  }
192
}
193
194
function updateSuggestionSelection(suggestions: NodeListOf<Element>): void {
195
  clearSuggestionSelection();
196
  
197
  if (selectedSuggestionIndex >= 0 && selectedSuggestionIndex < suggestions.length) {
198
    const selectedItem = suggestions[selectedSuggestionIndex] as HTMLElement;
199
    selectedItem.setAttribute('aria-selected', 'true');
200
    selectedItem.scrollIntoView({ block: 'nearest' });
201
  }
202
}
203
204
function clearSuggestionSelection(): void {
205
  elements.wordSuggestions.querySelectorAll('.suggestion-item').forEach(item => {
206
    item.setAttribute('aria-selected', 'false');
207
  });
208
}
209
210
function selectWord(word: string): void {
211
  const wordIndex = state.wordlist.indexOf(word);
212
  
213
  if (wordIndex === -1) return;
214
  
215
  elements.wordInput.value = word;
216
  elements.wordInput.classList.remove('error');
217
  setStateFromIndex(wordIndex);
218
  updateDisplay();
219
  hideSuggestions();
220
  elements.wordInput.blur();
221
}
222
223
export function clearWordInput(): void {
224
  elements.wordInput.value = '';
225
  elements.wordInput.classList.remove('error');
226
  hideSuggestions();
227
}
228
229
export function syncWordInputFromState(): void {
230
  const index = state.boxes.reduce((acc, val, i) => acc + (val ? Math.pow(2, 11 - i) : 0), 0);
231
  
232
  if (index > 0 && index <= state.wordlist.length) {
233
    const word = state.wordlist[index - 1];
234
    if (elements.wordInput.value !== word) {
235
      elements.wordInput.value = word;
236
    }
237
  } else {
238
    if (elements.wordInput.value !== '') {
239
      elements.wordInput.value = '';
240
    }
241
  }
242
}
243