js_lis 2.0.8 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VirtualKeyboard.js +491 -350
- package/layouts.js +104 -15
- package/main.js +8 -42
- package/package.json +1 -1
- package/style/kbstyle.js +6 -7
package/VirtualKeyboard.js
CHANGED
|
@@ -14,10 +14,17 @@ export class VirtualKeyboard {
|
|
|
14
14
|
this.shiftActive = false;
|
|
15
15
|
this.capsLockActive = false;
|
|
16
16
|
this.isScrambled = false;
|
|
17
|
-
this.isVirtualKeyboardActive = false;
|
|
17
|
+
this.isVirtualKeyboardActive = false;
|
|
18
18
|
|
|
19
19
|
this.inputPlaintextBuffers = new WeakMap();
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
// FIX BUG-2: เก็บ bound reference เพื่อให้ removeEventListener ทำงานได้
|
|
22
|
+
this._boundDrag = this.drag.bind(this);
|
|
23
|
+
this._boundStopDrag = this.stopDrag.bind(this);
|
|
24
|
+
|
|
25
|
+
// FIX WARN-3: เก็บ flag ว่า listeners ถูก add ไปแล้วหรือยัง
|
|
26
|
+
this._listenersInitialized = false;
|
|
27
|
+
|
|
21
28
|
this.initialize();
|
|
22
29
|
}
|
|
23
30
|
|
|
@@ -25,7 +32,11 @@ export class VirtualKeyboard {
|
|
|
25
32
|
try {
|
|
26
33
|
injectCSS();
|
|
27
34
|
this.render();
|
|
28
|
-
|
|
35
|
+
// FIX WARN-3: เรียก initializeInputListeners ครั้งเดียวเท่านั้น
|
|
36
|
+
if (!this._listenersInitialized) {
|
|
37
|
+
this.initializeInputListeners();
|
|
38
|
+
this._listenersInitialized = true;
|
|
39
|
+
}
|
|
29
40
|
console.log("VirtualKeyboard initialized successfully.");
|
|
30
41
|
} catch (error) {
|
|
31
42
|
console.error("Error initializing VirtualKeyboard:", error);
|
|
@@ -65,41 +76,118 @@ export class VirtualKeyboard {
|
|
|
65
76
|
}
|
|
66
77
|
});
|
|
67
78
|
|
|
68
|
-
document.addEventListener(
|
|
79
|
+
document.addEventListener(
|
|
80
|
+
"focus",
|
|
81
|
+
(e) => {
|
|
69
82
|
const target = e.target;
|
|
70
83
|
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
|
|
71
84
|
this.setCurrentInput(target, !this.isVirtualKeyboardActive);
|
|
72
85
|
}
|
|
73
86
|
},
|
|
74
|
-
true
|
|
87
|
+
true,
|
|
75
88
|
);
|
|
76
89
|
|
|
77
|
-
//
|
|
90
|
+
// FIX BUG-4: handle paste event เพื่อ sync buffer
|
|
91
|
+
document.addEventListener("paste", (e) => {
|
|
92
|
+
const target = e.target;
|
|
93
|
+
if (
|
|
94
|
+
(target.tagName === "INPUT" || target.tagName === "TEXTAREA") &&
|
|
95
|
+
target === this.currentInput &&
|
|
96
|
+
!this.isVirtualKeyboardActive
|
|
97
|
+
) {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
const pasteText = (e.clipboardData || window.clipboardData).getData(
|
|
100
|
+
"text",
|
|
101
|
+
);
|
|
102
|
+
if (!pasteText) return;
|
|
103
|
+
|
|
104
|
+
const start = target.selectionStart;
|
|
105
|
+
const end = target.selectionEnd;
|
|
106
|
+
const buffer = this.getCurrentBuffer();
|
|
107
|
+
const newBuffer =
|
|
108
|
+
buffer.slice(0, start) + pasteText + buffer.slice(end);
|
|
109
|
+
this.setCurrentBuffer(newBuffer);
|
|
110
|
+
this.updateDisplayedValue(false);
|
|
111
|
+
|
|
112
|
+
const newCursorPos = start + pasteText.length;
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
target.setSelectionRange(newCursorPos, newCursorPos);
|
|
115
|
+
target.focus();
|
|
116
|
+
}, 0);
|
|
117
|
+
|
|
118
|
+
const event = new Event("input", { bubbles: true });
|
|
119
|
+
target.dispatchEvent(event);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// FIX BUG-4: handle cut event เพื่อ sync buffer
|
|
124
|
+
document.addEventListener("cut", (e) => {
|
|
125
|
+
const target = e.target;
|
|
126
|
+
if (
|
|
127
|
+
(target.tagName === "INPUT" || target.tagName === "TEXTAREA") &&
|
|
128
|
+
target === this.currentInput &&
|
|
129
|
+
!this.isVirtualKeyboardActive
|
|
130
|
+
) {
|
|
131
|
+
const start = target.selectionStart;
|
|
132
|
+
const end = target.selectionEnd;
|
|
133
|
+
if (start === end) return;
|
|
134
|
+
|
|
135
|
+
const buffer = this.getCurrentBuffer();
|
|
136
|
+
const selectedText = buffer.slice(start, end);
|
|
137
|
+
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
e.clipboardData.setData("text/plain", selectedText);
|
|
140
|
+
|
|
141
|
+
const newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
142
|
+
this.setCurrentBuffer(newBuffer);
|
|
143
|
+
this.updateDisplayedValue(false);
|
|
144
|
+
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
target.setSelectionRange(start, start);
|
|
147
|
+
target.focus();
|
|
148
|
+
}, 0);
|
|
149
|
+
|
|
150
|
+
const event = new Event("input", { bubbles: true });
|
|
151
|
+
target.dispatchEvent(event);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
78
155
|
document.addEventListener("input", (e) => {
|
|
79
156
|
const target = e.target;
|
|
80
|
-
if (
|
|
157
|
+
if (
|
|
158
|
+
(target.tagName === "INPUT" || target.tagName === "TEXTAREA") &&
|
|
159
|
+
target === this.currentInput
|
|
160
|
+
) {
|
|
81
161
|
if (!this.isVirtualKeyboardActive) {
|
|
82
162
|
this.handleRealKeyboardInput(target, e);
|
|
83
163
|
}
|
|
84
164
|
}
|
|
85
165
|
});
|
|
86
166
|
|
|
87
|
-
// Add real keyboard keydown listener for proper character tracking
|
|
88
167
|
document.addEventListener("keydown", (e) => {
|
|
89
168
|
const target = e.target;
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
169
|
+
if (
|
|
170
|
+
(target.tagName === "INPUT" || target.tagName === "TEXTAREA") &&
|
|
171
|
+
target === this.currentInput
|
|
172
|
+
) {
|
|
173
|
+
const navKeys = [
|
|
174
|
+
"ArrowLeft",
|
|
175
|
+
"ArrowRight",
|
|
176
|
+
"ArrowUp",
|
|
177
|
+
"ArrowDown",
|
|
178
|
+
"Home",
|
|
179
|
+
"End",
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
if (!navKeys.includes(e.code)) {
|
|
93
183
|
this.isVirtualKeyboardActive = false;
|
|
94
184
|
}
|
|
95
|
-
|
|
96
|
-
// Handle real keyboard input
|
|
185
|
+
|
|
97
186
|
if (!this.isVirtualKeyboardActive) {
|
|
98
187
|
this.handleRealKeyboardKeydown(target, e);
|
|
99
188
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.code)) {
|
|
189
|
+
|
|
190
|
+
if (navKeys.includes(e.code)) {
|
|
103
191
|
setTimeout(() => {
|
|
104
192
|
target.focus();
|
|
105
193
|
}, 0);
|
|
@@ -120,29 +208,14 @@ export class VirtualKeyboard {
|
|
|
120
208
|
|
|
121
209
|
this.currentInput = inputElement;
|
|
122
210
|
this.currentInput.classList.add("keyboard-active");
|
|
123
|
-
|
|
124
211
|
this.currentInput.focus();
|
|
125
|
-
|
|
126
|
-
// Ensure buffer initialized for this input
|
|
212
|
+
|
|
127
213
|
if (!this.inputPlaintextBuffers.has(this.currentInput)) {
|
|
128
|
-
const
|
|
129
|
-
let initial = "";
|
|
130
|
-
try {
|
|
131
|
-
if (ds && window.VKCrypto && typeof window.VKCrypto.decrypt === "function") {
|
|
132
|
-
initial = window.VKCrypto.decrypt(ds) || "";
|
|
133
|
-
} else {
|
|
134
|
-
initial = this.currentInput.value || "";
|
|
135
|
-
}
|
|
136
|
-
} catch (_) {
|
|
137
|
-
initial = this.currentInput.value || "";
|
|
138
|
-
}
|
|
214
|
+
const initial = this.currentInput.value || "";
|
|
139
215
|
this.inputPlaintextBuffers.set(this.currentInput, initial);
|
|
140
|
-
this.updateDomEncrypted(this.currentInput, initial);
|
|
141
216
|
}
|
|
142
|
-
// Sync visible with buffer (masking for password)
|
|
143
217
|
this.updateDisplayedValue();
|
|
144
|
-
|
|
145
|
-
// Only move cursor to end on initial focus, not during virtual keyboard operations
|
|
218
|
+
|
|
146
219
|
if (moveToEnd) {
|
|
147
220
|
setTimeout(() => {
|
|
148
221
|
const length = this.currentInput.value.length;
|
|
@@ -151,7 +224,6 @@ export class VirtualKeyboard {
|
|
|
151
224
|
}
|
|
152
225
|
}
|
|
153
226
|
|
|
154
|
-
// [1]
|
|
155
227
|
render() {
|
|
156
228
|
const keyboard = document.createElement("div");
|
|
157
229
|
keyboard.className = `virtual-keyboard ${this.currentLayout}`;
|
|
@@ -167,11 +239,11 @@ export class VirtualKeyboard {
|
|
|
167
239
|
|
|
168
240
|
const layoutSelector = document.createElement("select");
|
|
169
241
|
layoutSelector.id = "layout-selector";
|
|
170
|
-
layoutSelector.setAttribute("aria-label", "Select keyboard layout");
|
|
242
|
+
layoutSelector.setAttribute("aria-label", "Select keyboard layout");
|
|
171
243
|
layoutSelector.onchange = (e) => this.changeLayout(e.target.value);
|
|
172
244
|
|
|
173
|
-
const
|
|
174
|
-
|
|
245
|
+
const availableLayouts = ["full", "en", "th", "numpad", "symbols"];
|
|
246
|
+
availableLayouts.forEach((layout) => {
|
|
175
247
|
const option = document.createElement("option");
|
|
176
248
|
option.value = layout;
|
|
177
249
|
option.innerText = this.getLayoutName(layout);
|
|
@@ -180,7 +252,6 @@ export class VirtualKeyboard {
|
|
|
180
252
|
layoutSelector.value = this.currentLayout;
|
|
181
253
|
controlsContainer.appendChild(layoutSelector);
|
|
182
254
|
|
|
183
|
-
|
|
184
255
|
keyboard.appendChild(controlsContainer);
|
|
185
256
|
|
|
186
257
|
const layout = this.layouts[this.currentLayout];
|
|
@@ -194,21 +265,38 @@ export class VirtualKeyboard {
|
|
|
194
265
|
keyElement.className = "keyboard-key key";
|
|
195
266
|
keyElement.textContent = key;
|
|
196
267
|
keyElement.type = "button";
|
|
197
|
-
|
|
198
268
|
keyElement.dataset.key = key;
|
|
199
269
|
|
|
270
|
+
// FIX IMP-4: เพิ่ม aria-label บนทุก key
|
|
271
|
+
keyElement.setAttribute(
|
|
272
|
+
"aria-label",
|
|
273
|
+
key === " "
|
|
274
|
+
? "spacebar"
|
|
275
|
+
: key === "backspace" || key === "Backspace"
|
|
276
|
+
? "backspace"
|
|
277
|
+
: key,
|
|
278
|
+
);
|
|
279
|
+
|
|
200
280
|
if (index >= row.length - 4) {
|
|
201
281
|
keyElement.classList.add("concat-keys");
|
|
202
282
|
}
|
|
203
283
|
|
|
204
284
|
if (key === " ") {
|
|
205
|
-
keyElement.className += "
|
|
206
|
-
keyElement.innerHTML =
|
|
285
|
+
keyElement.className += " spacebar";
|
|
286
|
+
keyElement.innerHTML = "";
|
|
207
287
|
}
|
|
208
288
|
|
|
209
289
|
if (key === "backspace" || key === "Backspace") {
|
|
210
290
|
keyElement.className += " backspacew";
|
|
211
|
-
keyElement.innerHTML = '<i class="fa fa-backspace"></i
|
|
291
|
+
keyElement.innerHTML = '<i class="fa fa-backspace"></i>';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// FIX WARN-2: restore active state หลัง re-render
|
|
295
|
+
if (key === "Caps 🄰" && this.capsLockActive) {
|
|
296
|
+
keyElement.classList.add("active", "bg-gray-400");
|
|
297
|
+
}
|
|
298
|
+
if (key === "Shift ⇧" && this.shiftActive) {
|
|
299
|
+
keyElement.classList.add("active", "bg-gray-400");
|
|
212
300
|
}
|
|
213
301
|
|
|
214
302
|
keyElement.onclick = (e) => {
|
|
@@ -230,16 +318,16 @@ export class VirtualKeyboard {
|
|
|
230
318
|
this.container.innerHTML = "";
|
|
231
319
|
this.container.appendChild(keyboard);
|
|
232
320
|
|
|
321
|
+
// FIX BUG-2: ใช้ bound reference แทน bind() inline
|
|
233
322
|
keyboard.addEventListener("mousedown", (event) => this.startDrag(event));
|
|
234
323
|
}
|
|
235
324
|
|
|
236
|
-
//
|
|
237
|
-
|
|
325
|
+
// FIX BUG-3 & WARN-5: เปลี่ยนเป็น sync, จัดการ isVirtualKeyboardActive อย่างถูกต้อง
|
|
326
|
+
handleKeyPress(keyPressed) {
|
|
238
327
|
if (!this.currentInput) return;
|
|
239
|
-
|
|
240
|
-
// Set flag to indicate virtual keyboard is active
|
|
328
|
+
|
|
241
329
|
this.isVirtualKeyboardActive = true;
|
|
242
|
-
|
|
330
|
+
|
|
243
331
|
const start = this.currentInput.selectionStart;
|
|
244
332
|
const end = this.currentInput.selectionEnd;
|
|
245
333
|
const buffer = this.getCurrentBuffer();
|
|
@@ -248,9 +336,7 @@ export class VirtualKeyboard {
|
|
|
248
336
|
const isShiftActive = this.shiftActive;
|
|
249
337
|
|
|
250
338
|
const convertToCorrectCase = (char) => {
|
|
251
|
-
if (isCapsActive || isShiftActive)
|
|
252
|
-
return char.toUpperCase();
|
|
253
|
-
}
|
|
339
|
+
if (isCapsActive || isShiftActive) return char.toUpperCase();
|
|
254
340
|
return char.toLowerCase();
|
|
255
341
|
};
|
|
256
342
|
|
|
@@ -260,71 +346,32 @@ export class VirtualKeyboard {
|
|
|
260
346
|
}
|
|
261
347
|
|
|
262
348
|
if (keyPressed === "scr" || keyPressed === "scrambled") {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
this.unscrambleKeys()
|
|
266
|
-
|
|
267
|
-
this.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
this.scrambleThaiKeys();
|
|
282
|
-
}
|
|
349
|
+
const layoutScrambleMap = {
|
|
350
|
+
en: () =>
|
|
351
|
+
this.isScrambled ? this.unscrambleKeys() : this.scrambleEnglishKeys(),
|
|
352
|
+
th: () =>
|
|
353
|
+
this.isScrambled ? this.unscrambleKeys() : this.scrambleThaiKeys(),
|
|
354
|
+
numpad: () =>
|
|
355
|
+
this.isScrambled ? this.unscrambleKeys() : this.scrambleKeyboard(),
|
|
356
|
+
symbols: () =>
|
|
357
|
+
this.isScrambled ? this.unscrambleKeys() : this.scrambleSymbols(),
|
|
358
|
+
full: () =>
|
|
359
|
+
this.isScrambled
|
|
360
|
+
? this.unscrambleKeys()
|
|
361
|
+
: this.scrambleFullLayoutKeys(),
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const handler = layoutScrambleMap[this.currentLayout];
|
|
365
|
+
if (handler) {
|
|
366
|
+
handler();
|
|
283
367
|
this.isScrambled = !this.isScrambled;
|
|
284
368
|
document
|
|
285
|
-
.querySelectorAll('.key[data-key="scrambled"]')
|
|
369
|
+
.querySelectorAll('.key[data-key="scr"], .key[data-key="scrambled"]')
|
|
286
370
|
.forEach((key) => {
|
|
287
371
|
key.classList.toggle("active", this.isScrambled);
|
|
288
372
|
});
|
|
289
373
|
}
|
|
290
374
|
|
|
291
|
-
if (this.currentLayout === "numpad") {
|
|
292
|
-
if (this.isScrambled) {
|
|
293
|
-
this.unscrambleKeys();
|
|
294
|
-
} else {
|
|
295
|
-
this.scrambleKeyboard();
|
|
296
|
-
}
|
|
297
|
-
this.isScrambled = !this.isScrambled;
|
|
298
|
-
document.querySelectorAll('.key[data-key="scr"]').forEach((key) => {
|
|
299
|
-
key.classList.toggle("active", this.isScrambled);
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (this.currentLayout === "symbols") {
|
|
304
|
-
if (this.isScrambled) {
|
|
305
|
-
this.unscrambleKeys();
|
|
306
|
-
} else {
|
|
307
|
-
this.scrambleSymbols();
|
|
308
|
-
}
|
|
309
|
-
this.isScrambled = !this.isScrambled;
|
|
310
|
-
document.querySelectorAll('.key[data-key="scr"]').forEach((key) => {
|
|
311
|
-
key.classList.toggle("active", this.isScrambled);
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (this.currentLayout === "full") {
|
|
316
|
-
if (this.isScrambled) {
|
|
317
|
-
this.unscrambleKeys();
|
|
318
|
-
} else {
|
|
319
|
-
this.scrambleFullLayoutKeys();
|
|
320
|
-
}
|
|
321
|
-
this.isScrambled = !this.isScrambled;
|
|
322
|
-
document
|
|
323
|
-
.querySelectorAll('.key[data-key="scrambled"]')
|
|
324
|
-
.forEach((key) => {
|
|
325
|
-
key.classList.toggle("active", this.isScrambled);
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
375
|
this.isVirtualKeyboardActive = false;
|
|
329
376
|
return;
|
|
330
377
|
}
|
|
@@ -337,11 +384,10 @@ export class VirtualKeyboard {
|
|
|
337
384
|
this.currentInput.setSelectionRange(0, 0);
|
|
338
385
|
this.currentInput.focus();
|
|
339
386
|
}, 5);
|
|
340
|
-
// Skip the general cursor repositioning for navigation keys
|
|
341
387
|
this.isVirtualKeyboardActive = false;
|
|
342
388
|
return;
|
|
343
389
|
|
|
344
|
-
case "END":
|
|
390
|
+
case "END": {
|
|
345
391
|
const bufferLengthEnd = this.getCurrentBuffer().length;
|
|
346
392
|
this.currentInput.setSelectionRange(bufferLengthEnd, bufferLengthEnd);
|
|
347
393
|
this.currentInput.focus();
|
|
@@ -349,9 +395,9 @@ export class VirtualKeyboard {
|
|
|
349
395
|
this.currentInput.setSelectionRange(bufferLengthEnd, bufferLengthEnd);
|
|
350
396
|
this.currentInput.focus();
|
|
351
397
|
}, 5);
|
|
352
|
-
// Skip the general cursor repositioning for navigation keys
|
|
353
398
|
this.isVirtualKeyboardActive = false;
|
|
354
399
|
return;
|
|
400
|
+
}
|
|
355
401
|
|
|
356
402
|
case "Backspace":
|
|
357
403
|
case "backspace":
|
|
@@ -361,12 +407,11 @@ export class VirtualKeyboard {
|
|
|
361
407
|
const newCursorPos = start - 1;
|
|
362
408
|
this.updateDisplayedValue(false);
|
|
363
409
|
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
364
|
-
} else {
|
|
410
|
+
} else if (start !== end) {
|
|
365
411
|
const newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
366
412
|
this.setCurrentBuffer(newBuffer);
|
|
367
|
-
const newCursorPos = start;
|
|
368
413
|
this.updateDisplayedValue(false);
|
|
369
|
-
this.currentInput.setSelectionRange(
|
|
414
|
+
this.currentInput.setSelectionRange(start, start);
|
|
370
415
|
}
|
|
371
416
|
break;
|
|
372
417
|
|
|
@@ -374,24 +419,22 @@ export class VirtualKeyboard {
|
|
|
374
419
|
if (start === end && start < buffer.length) {
|
|
375
420
|
const newBuffer = buffer.slice(0, start) + buffer.slice(end + 1);
|
|
376
421
|
this.setCurrentBuffer(newBuffer);
|
|
377
|
-
const newCursorPos = start;
|
|
378
422
|
this.updateDisplayedValue(false);
|
|
379
|
-
this.currentInput.setSelectionRange(
|
|
380
|
-
} else {
|
|
423
|
+
this.currentInput.setSelectionRange(start, start);
|
|
424
|
+
} else if (start !== end) {
|
|
381
425
|
const newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
382
426
|
this.setCurrentBuffer(newBuffer);
|
|
383
|
-
const newCursorPos = start;
|
|
384
427
|
this.updateDisplayedValue(false);
|
|
385
|
-
this.currentInput.setSelectionRange(
|
|
428
|
+
this.currentInput.setSelectionRange(start, start);
|
|
386
429
|
}
|
|
387
430
|
break;
|
|
388
431
|
|
|
389
432
|
case "Space":
|
|
390
|
-
|
|
433
|
+
this.insertText(" ");
|
|
391
434
|
break;
|
|
392
435
|
|
|
393
436
|
case "Tab ↹":
|
|
394
|
-
|
|
437
|
+
this.insertText("\t");
|
|
395
438
|
break;
|
|
396
439
|
|
|
397
440
|
case "Enter":
|
|
@@ -401,12 +444,17 @@ export class VirtualKeyboard {
|
|
|
401
444
|
const newCursorPos = start + 1;
|
|
402
445
|
this.updateDisplayedValue(false);
|
|
403
446
|
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
404
|
-
} else if (
|
|
447
|
+
} else if (
|
|
448
|
+
this.currentInput.tagName === "INPUT" ||
|
|
449
|
+
this.currentInput.type === "password" ||
|
|
450
|
+
this.currentInput.type === "text"
|
|
451
|
+
) {
|
|
405
452
|
if (this.currentInput.form) {
|
|
406
453
|
const submitButton = this.currentInput.form.querySelector(
|
|
407
|
-
'input[type="submit"], button[type="submit"], button[type="button"], button[onclick]'
|
|
454
|
+
'input[type="submit"], button[type="submit"], button[type="button"], button[onclick]',
|
|
408
455
|
);
|
|
409
|
-
if (submitButton) submitButton.click();
|
|
456
|
+
if (submitButton) submitButton.click();
|
|
457
|
+
else this.currentInput.form.submit();
|
|
410
458
|
} else {
|
|
411
459
|
const newBuffer = buffer + "\n";
|
|
412
460
|
this.setCurrentBuffer(newBuffer);
|
|
@@ -423,213 +471,245 @@ export class VirtualKeyboard {
|
|
|
423
471
|
this.toggleShift();
|
|
424
472
|
break;
|
|
425
473
|
|
|
426
|
-
case "←":
|
|
474
|
+
case "←": {
|
|
427
475
|
const leftNewPos = Math.max(0, start - 1);
|
|
428
476
|
this.currentInput.setSelectionRange(leftNewPos, leftNewPos);
|
|
429
|
-
|
|
477
|
+
this.isVirtualKeyboardActive = false;
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
430
480
|
|
|
431
|
-
case "→":
|
|
481
|
+
case "→": {
|
|
432
482
|
const bufferLength = this.getCurrentBuffer().length;
|
|
433
483
|
const rightNewPos = Math.min(bufferLength, start + 1);
|
|
434
484
|
this.currentInput.setSelectionRange(rightNewPos, rightNewPos);
|
|
435
|
-
|
|
485
|
+
this.isVirtualKeyboardActive = false;
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
436
488
|
|
|
437
489
|
case "↑":
|
|
438
|
-
case "↓":
|
|
439
|
-
// Use the actual buffer content instead of displayed value
|
|
490
|
+
case "↓": {
|
|
440
491
|
const actualText = this.getCurrentBuffer();
|
|
441
492
|
const actualLines = actualText.substring(0, start).split("\n");
|
|
442
493
|
const actualCurrentLineIndex = actualLines.length - 1;
|
|
443
494
|
const actualCurrentLine = actualLines[actualCurrentLineIndex];
|
|
444
|
-
const actualColumnIndex =
|
|
495
|
+
const actualColumnIndex =
|
|
496
|
+
start - actualText.lastIndexOf("\n", start - 1) - 1;
|
|
445
497
|
|
|
446
498
|
if (keyPressed === "↑" && actualCurrentLineIndex > 0) {
|
|
447
|
-
// Move to previous line
|
|
448
499
|
const prevLineLength = actualLines[actualCurrentLineIndex - 1].length;
|
|
449
|
-
const upNewPos =
|
|
450
|
-
|
|
500
|
+
const upNewPos =
|
|
501
|
+
start -
|
|
502
|
+
actualCurrentLine.length -
|
|
503
|
+
1 -
|
|
504
|
+
Math.max(0, actualColumnIndex - prevLineLength);
|
|
505
|
+
this.currentInput.setSelectionRange(
|
|
506
|
+
Math.max(0, upNewPos),
|
|
507
|
+
Math.max(0, upNewPos),
|
|
508
|
+
);
|
|
451
509
|
} else if (keyPressed === "↓") {
|
|
452
|
-
// Move to next line
|
|
453
510
|
const remainingText = actualText.substring(start);
|
|
454
511
|
const nextLineBreak = remainingText.indexOf("\n");
|
|
455
|
-
|
|
456
512
|
if (nextLineBreak !== -1) {
|
|
457
|
-
|
|
458
|
-
|
|
513
|
+
const textAfterNextLineBreak = remainingText.substring(
|
|
514
|
+
nextLineBreak + 1,
|
|
515
|
+
);
|
|
459
516
|
const nextLineEnd = textAfterNextLineBreak.indexOf("\n");
|
|
460
|
-
const nextLineLength =
|
|
461
|
-
|
|
462
|
-
const downNewPos =
|
|
517
|
+
const nextLineLength =
|
|
518
|
+
nextLineEnd === -1 ? textAfterNextLineBreak.length : nextLineEnd;
|
|
519
|
+
const downNewPos =
|
|
520
|
+
start +
|
|
521
|
+
nextLineBreak +
|
|
522
|
+
1 +
|
|
523
|
+
Math.min(actualColumnIndex, nextLineLength);
|
|
463
524
|
this.currentInput.setSelectionRange(downNewPos, downNewPos);
|
|
464
525
|
}
|
|
465
|
-
// If no next line, do nothing (cursor stays at current position)
|
|
466
526
|
}
|
|
467
|
-
|
|
527
|
+
|
|
468
528
|
this.currentInput.focus();
|
|
469
|
-
// Skip the general cursor repositioning for arrow keys
|
|
470
529
|
this.isVirtualKeyboardActive = false;
|
|
471
|
-
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
472
532
|
|
|
473
|
-
default:
|
|
474
|
-
const textvalue =
|
|
475
|
-
|
|
533
|
+
default: {
|
|
534
|
+
const textvalue = convertToCorrectCase(keyPressed);
|
|
535
|
+
this.insertText(textvalue);
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
476
538
|
}
|
|
477
539
|
|
|
478
540
|
if (isShiftActive && !isCapsActive) this.toggleShift();
|
|
479
541
|
|
|
480
542
|
this.currentInput.focus();
|
|
481
543
|
|
|
544
|
+
// FIX BUG-3: reset flag หลัง setTimeout เพื่อป้องกัน real keyboard listener เข้ามาแทรก
|
|
482
545
|
setTimeout(() => {
|
|
483
|
-
this.currentInput
|
|
484
|
-
|
|
485
|
-
|
|
546
|
+
if (this.currentInput) {
|
|
547
|
+
this.currentInput.focus();
|
|
548
|
+
const cursorPos = this.currentInput.selectionStart;
|
|
549
|
+
this.currentInput.setSelectionRange(cursorPos, cursorPos);
|
|
550
|
+
}
|
|
551
|
+
this.isVirtualKeyboardActive = false;
|
|
486
552
|
}, 0);
|
|
487
|
-
|
|
553
|
+
|
|
488
554
|
this.currentInput.focus();
|
|
489
555
|
const event = new Event("input", { bubbles: true });
|
|
490
556
|
this.currentInput.dispatchEvent(event);
|
|
491
|
-
|
|
492
|
-
// Reset the flag after virtual keyboard operation is complete
|
|
493
|
-
this.isVirtualKeyboardActive = false;
|
|
494
557
|
}
|
|
495
558
|
|
|
496
|
-
|
|
497
|
-
async insertText(text) {
|
|
559
|
+
insertText(text) {
|
|
498
560
|
const start = this.currentInput.selectionStart;
|
|
499
561
|
const end = this.currentInput.selectionEnd;
|
|
500
|
-
const textvalue = text;
|
|
501
562
|
|
|
502
563
|
const buffer = this.getCurrentBuffer();
|
|
503
|
-
const newBuffer = buffer.slice(0, start) +
|
|
564
|
+
const newBuffer = buffer.slice(0, start) + text + buffer.slice(end);
|
|
504
565
|
this.setCurrentBuffer(newBuffer);
|
|
505
|
-
|
|
506
|
-
const newCursorPos = start +
|
|
507
|
-
|
|
566
|
+
|
|
567
|
+
const newCursorPos = start + text.length;
|
|
508
568
|
this.updateDisplayedValue(false);
|
|
509
|
-
|
|
510
569
|
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
511
570
|
}
|
|
512
571
|
|
|
513
|
-
// Helper: get current plaintext buffer for active input
|
|
514
572
|
getCurrentBuffer() {
|
|
515
573
|
if (!this.currentInput) return "";
|
|
516
574
|
return this.inputPlaintextBuffers.get(this.currentInput) || "";
|
|
517
575
|
}
|
|
518
576
|
|
|
519
|
-
|
|
577
|
+
getBufferForInput(inputElement) {
|
|
578
|
+
if (!inputElement) return "";
|
|
579
|
+
const bufferedValue = this.inputPlaintextBuffers.get(inputElement);
|
|
580
|
+
if (typeof bufferedValue === "string") {
|
|
581
|
+
return bufferedValue;
|
|
582
|
+
}
|
|
583
|
+
return inputElement.value || "";
|
|
584
|
+
}
|
|
585
|
+
|
|
520
586
|
setCurrentBuffer(newBuffer) {
|
|
521
587
|
if (!this.currentInput) return;
|
|
522
588
|
this.inputPlaintextBuffers.set(this.currentInput, newBuffer);
|
|
523
|
-
this.updateDomEncrypted(this.currentInput, newBuffer);
|
|
524
589
|
}
|
|
525
590
|
|
|
526
|
-
// Helper: reflect masked value in the visible input/textarea
|
|
527
591
|
updateDisplayedValue(preserveCursor = true) {
|
|
528
592
|
if (!this.currentInput) return;
|
|
529
593
|
const buffer = this.getCurrentBuffer();
|
|
530
594
|
const isPassword = this.currentInput.type === "password";
|
|
531
595
|
const maskChar = "•";
|
|
532
|
-
|
|
533
|
-
// Store current cursor position only if we want to preserve it
|
|
596
|
+
|
|
534
597
|
const currentStart = preserveCursor ? this.currentInput.selectionStart : 0;
|
|
535
598
|
const currentEnd = preserveCursor ? this.currentInput.selectionEnd : 0;
|
|
536
|
-
|
|
537
|
-
this.currentInput.value = isPassword
|
|
538
|
-
|
|
539
|
-
|
|
599
|
+
|
|
600
|
+
this.currentInput.value = isPassword
|
|
601
|
+
? maskChar.repeat(buffer.length)
|
|
602
|
+
: buffer;
|
|
603
|
+
|
|
540
604
|
if (preserveCursor) {
|
|
541
605
|
this.currentInput.setSelectionRange(currentStart, currentEnd);
|
|
542
606
|
}
|
|
543
607
|
}
|
|
544
608
|
|
|
545
|
-
// Handle real keyboard keydown events to track actual input before masking
|
|
546
609
|
handleRealKeyboardKeydown(input, event) {
|
|
547
610
|
if (!input) return;
|
|
548
|
-
|
|
611
|
+
|
|
549
612
|
const key = event.key;
|
|
550
613
|
const start = input.selectionStart;
|
|
551
614
|
const end = input.selectionEnd;
|
|
552
615
|
const buffer = this.getCurrentBuffer();
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
616
|
+
|
|
617
|
+
if (event.ctrlKey || event.altKey || event.metaKey) {
|
|
618
|
+
// FIX BUG-6: handle Ctrl+A
|
|
619
|
+
if ((event.ctrlKey || event.metaKey) && key === "a") {
|
|
620
|
+
// Allow default selection — no buffer change needed
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
// FIX BUG-6: handle Ctrl+Z (undo) — best-effort: เราไม่เก็บ history จึง ignore
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (
|
|
628
|
+
[
|
|
629
|
+
"Shift",
|
|
630
|
+
"Control",
|
|
631
|
+
"Alt",
|
|
632
|
+
"Meta",
|
|
633
|
+
"CapsLock",
|
|
634
|
+
"Tab",
|
|
635
|
+
"Escape",
|
|
636
|
+
"ArrowLeft",
|
|
637
|
+
"ArrowRight",
|
|
638
|
+
"ArrowUp",
|
|
639
|
+
"ArrowDown",
|
|
640
|
+
"Home",
|
|
641
|
+
"End",
|
|
642
|
+
"F1",
|
|
643
|
+
"F2",
|
|
644
|
+
"F3",
|
|
645
|
+
"F4",
|
|
646
|
+
"F5",
|
|
647
|
+
"F6",
|
|
648
|
+
"F7",
|
|
649
|
+
"F8",
|
|
650
|
+
"F9",
|
|
651
|
+
"F10",
|
|
652
|
+
"F11",
|
|
653
|
+
"F12",
|
|
654
|
+
].includes(key)
|
|
655
|
+
) {
|
|
559
656
|
return;
|
|
560
657
|
}
|
|
561
|
-
|
|
658
|
+
|
|
562
659
|
let newBuffer = buffer;
|
|
563
|
-
|
|
564
|
-
if (key ===
|
|
660
|
+
|
|
661
|
+
if (key === "Backspace") {
|
|
565
662
|
if (start === end && start > 0) {
|
|
566
|
-
// Single character deletion
|
|
567
663
|
newBuffer = buffer.slice(0, start - 1) + buffer.slice(start);
|
|
568
664
|
} else if (start !== end) {
|
|
569
|
-
// Range deletion
|
|
570
665
|
newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
571
666
|
}
|
|
572
|
-
|
|
573
|
-
// Prevent default and update manually
|
|
667
|
+
|
|
574
668
|
event.preventDefault();
|
|
575
669
|
this.setCurrentBuffer(newBuffer);
|
|
576
670
|
this.updateDisplayedValue(false);
|
|
577
|
-
|
|
578
|
-
// Set cursor position
|
|
671
|
+
|
|
579
672
|
const newCursorPos = start === end ? Math.max(0, start - 1) : start;
|
|
580
673
|
setTimeout(() => {
|
|
581
674
|
input.setSelectionRange(newCursorPos, newCursorPos);
|
|
582
675
|
input.focus();
|
|
583
676
|
}, 0);
|
|
584
|
-
|
|
585
|
-
} else if (key === 'Delete') {
|
|
677
|
+
} else if (key === "Delete") {
|
|
586
678
|
if (start === end && start < buffer.length) {
|
|
587
|
-
// Single character deletion forward
|
|
588
679
|
newBuffer = buffer.slice(0, start) + buffer.slice(start + 1);
|
|
589
680
|
} else if (start !== end) {
|
|
590
|
-
// Range deletion
|
|
591
681
|
newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
592
682
|
}
|
|
593
|
-
|
|
594
|
-
// Prevent default and update manually
|
|
683
|
+
|
|
595
684
|
event.preventDefault();
|
|
596
685
|
this.setCurrentBuffer(newBuffer);
|
|
597
686
|
this.updateDisplayedValue(false);
|
|
598
|
-
|
|
599
|
-
// Set cursor position
|
|
687
|
+
|
|
600
688
|
setTimeout(() => {
|
|
601
689
|
input.setSelectionRange(start, start);
|
|
602
690
|
input.focus();
|
|
603
691
|
}, 0);
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
// Prevent default and update manually
|
|
692
|
+
} else if (key === "Enter") {
|
|
693
|
+
if (input.tagName === "TEXTAREA") {
|
|
694
|
+
newBuffer = buffer.slice(0, start) + "\n" + buffer.slice(end);
|
|
695
|
+
|
|
610
696
|
event.preventDefault();
|
|
611
697
|
this.setCurrentBuffer(newBuffer);
|
|
612
698
|
this.updateDisplayedValue(false);
|
|
613
|
-
|
|
614
|
-
// Set cursor position
|
|
699
|
+
|
|
615
700
|
const newCursorPos = start + 1;
|
|
616
701
|
setTimeout(() => {
|
|
617
702
|
input.setSelectionRange(newCursorPos, newCursorPos);
|
|
618
703
|
input.focus();
|
|
619
704
|
}, 0);
|
|
620
705
|
}
|
|
621
|
-
// For input fields, let the default behavior handle form submission
|
|
622
|
-
|
|
623
706
|
} else if (key.length === 1) {
|
|
624
|
-
// Regular character input
|
|
625
707
|
newBuffer = buffer.slice(0, start) + key + buffer.slice(end);
|
|
626
|
-
|
|
627
|
-
// Prevent default and update manually
|
|
708
|
+
|
|
628
709
|
event.preventDefault();
|
|
629
710
|
this.setCurrentBuffer(newBuffer);
|
|
630
711
|
this.updateDisplayedValue(false);
|
|
631
|
-
|
|
632
|
-
// Set cursor position
|
|
712
|
+
|
|
633
713
|
const newCursorPos = start + key.length;
|
|
634
714
|
setTimeout(() => {
|
|
635
715
|
input.setSelectionRange(newCursorPos, newCursorPos);
|
|
@@ -638,153 +718,200 @@ export class VirtualKeyboard {
|
|
|
638
718
|
}
|
|
639
719
|
}
|
|
640
720
|
|
|
641
|
-
//
|
|
721
|
+
// FIX BUG-4: handleRealKeyboardInput ทำหน้าที่ sync buffer เมื่อ autocomplete / IME
|
|
642
722
|
handleRealKeyboardInput(input, event) {
|
|
643
|
-
if (!input) return;
|
|
644
|
-
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
723
|
+
if (!input || !event) return;
|
|
724
|
+
|
|
725
|
+
// กรณี autocomplete, IME composition, หรือ browser fill ที่ไม่ผ่าน keydown
|
|
726
|
+
// sync จาก actual value กลับมาใส่ buffer
|
|
727
|
+
if (
|
|
728
|
+
event.inputType &&
|
|
729
|
+
event.inputType.startsWith("insertReplacementText")
|
|
730
|
+
) {
|
|
731
|
+
this.inputPlaintextBuffers.set(input, input.value);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
650
734
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const cipher = plaintext.length ? crypto.encrypt(plaintext) : "";
|
|
658
|
-
input.dataset.encrypted = cipher;
|
|
659
|
-
} else {
|
|
660
|
-
// No encryption function available — don't store anything
|
|
661
|
-
console.warn("Encryption unavailable. Not storing plaintext.");
|
|
662
|
-
input.dataset.encrypted = "";
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
} catch (err) {
|
|
666
|
-
console.error("Failed to encrypt input:", err);
|
|
667
|
-
input.dataset.encrypted = "";
|
|
735
|
+
// IME composition end
|
|
736
|
+
if (
|
|
737
|
+
event.isComposing === false &&
|
|
738
|
+
event.inputType === "insertCompositionText"
|
|
739
|
+
) {
|
|
740
|
+
this.inputPlaintextBuffers.set(input, input.value);
|
|
668
741
|
}
|
|
669
742
|
}
|
|
670
743
|
|
|
671
|
-
// [4]
|
|
672
744
|
unscrambleKeys() {
|
|
673
|
-
this.keys = this.originalKeys;
|
|
674
745
|
this.render();
|
|
675
746
|
}
|
|
676
747
|
|
|
677
|
-
//
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
748
|
+
// FIX BUG-1: รวม toggleCapsLock และ toggleShift ให้มี updateKeyContent เดียว
|
|
749
|
+
// ลบเมธอด updateKeyContent ที่ซ้ำกันออก และใช้เมธอดเดียวที่รับ parameter
|
|
750
|
+
_updateAllKeyContent(isActive) {
|
|
751
|
+
const layoutMap = {
|
|
752
|
+
th: this.ThaiAlphabetShift,
|
|
753
|
+
en: this.EngAlphabetShift,
|
|
754
|
+
full: this.FullAlphabetShift,
|
|
755
|
+
};
|
|
756
|
+
const layout = layoutMap[this.currentLayout];
|
|
685
757
|
|
|
686
758
|
document.querySelectorAll(".key").forEach((key) => {
|
|
687
|
-
const isLetter =
|
|
759
|
+
const isLetter =
|
|
760
|
+
key.dataset.key &&
|
|
761
|
+
key.dataset.key.length === 1 &&
|
|
762
|
+
/[a-zA-Zก-๙]/.test(key.dataset.key);
|
|
688
763
|
|
|
689
764
|
if (isLetter) {
|
|
690
|
-
key.textContent =
|
|
765
|
+
key.textContent = isActive
|
|
691
766
|
? key.dataset.key.toUpperCase()
|
|
692
767
|
: key.dataset.key.toLowerCase();
|
|
693
768
|
}
|
|
694
|
-
|
|
769
|
+
|
|
770
|
+
if (!layout) return;
|
|
771
|
+
|
|
772
|
+
const currentChar = key.textContent.trim();
|
|
773
|
+
if (isActive && layout[currentChar]) {
|
|
774
|
+
key.textContent = layout[currentChar];
|
|
775
|
+
key.dataset.key = layout[currentChar];
|
|
776
|
+
} else if (!isActive && Object.values(layout).includes(currentChar)) {
|
|
777
|
+
const originalKey = Object.keys(layout).find(
|
|
778
|
+
(k) => layout[k] === currentChar,
|
|
779
|
+
);
|
|
780
|
+
if (originalKey) {
|
|
781
|
+
key.textContent = originalKey;
|
|
782
|
+
key.dataset.key = originalKey;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
695
785
|
});
|
|
696
786
|
}
|
|
697
|
-
updateKeyContent(key, capsLockActive) {
|
|
698
|
-
const currentChar = key.textContent.trim();
|
|
699
787
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (capsLockActive && layout[currentChar]) {
|
|
711
|
-
key.textContent = layout[currentChar];
|
|
712
|
-
key.dataset.key = layout[currentChar];
|
|
713
|
-
} else if (!capsLockActive && Object.values(layout).includes(currentChar)) {
|
|
714
|
-
const originalKey = Object.keys(layout).find((k) => layout[k] === currentChar);
|
|
715
|
-
if (originalKey) {
|
|
716
|
-
key.textContent = originalKey;
|
|
717
|
-
key.dataset.key = originalKey;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
788
|
+
toggleCapsLock() {
|
|
789
|
+
this.capsLockActive = !this.capsLockActive;
|
|
790
|
+
|
|
791
|
+
document.querySelectorAll('.key[data-key="Caps 🄰"]').forEach((key) => {
|
|
792
|
+
key.classList.toggle("active", this.capsLockActive);
|
|
793
|
+
key.classList.toggle("bg-gray-400", this.capsLockActive);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// FIX BUG-1: ใช้ _updateAllKeyContent แทน updateKeyContent ที่ซ้ำกัน
|
|
797
|
+
this._updateAllKeyContent(this.capsLockActive);
|
|
720
798
|
}
|
|
721
799
|
|
|
722
|
-
// [6]
|
|
723
800
|
toggleShift() {
|
|
724
801
|
if (this.capsLockActive) {
|
|
725
802
|
return this.toggleCapsLock();
|
|
726
803
|
}
|
|
727
804
|
|
|
728
805
|
this.shiftActive = !this.shiftActive;
|
|
806
|
+
|
|
729
807
|
document.querySelectorAll('.key[data-key="Shift ⇧"]').forEach((key) => {
|
|
730
808
|
key.classList.toggle("active", this.shiftActive);
|
|
731
809
|
key.classList.toggle("bg-gray-400", this.shiftActive);
|
|
732
810
|
});
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
if (isLetter) {
|
|
738
|
-
key.textContent = this.shiftActive
|
|
739
|
-
? key.dataset.key.toUpperCase()
|
|
740
|
-
: key.dataset.key.toLowerCase();
|
|
741
|
-
}
|
|
742
|
-
this.updateKeyContent(key, this.shiftActive);
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
updateKeyContent(key, shiftActive) {
|
|
746
|
-
const currentChar = key.textContent.trim();
|
|
747
|
-
|
|
748
|
-
const layouts = {
|
|
749
|
-
th: this.ThaiAlphabetShift,
|
|
750
|
-
en: this.EngAlphabetShift,
|
|
751
|
-
enSc: this.EngAlphabetShift,
|
|
752
|
-
full: this.FullAlphabetShift,
|
|
753
|
-
};
|
|
754
|
-
const layout = layouts[this.currentLayout];
|
|
755
|
-
|
|
756
|
-
if (!layout) return;
|
|
757
|
-
|
|
758
|
-
if (shiftActive && layout[currentChar]) {
|
|
759
|
-
key.textContent = layout[currentChar];
|
|
760
|
-
key.dataset.key = layout[currentChar];
|
|
761
|
-
} else if (!shiftActive && Object.values(layout).includes(currentChar)) {
|
|
762
|
-
const originalKey = Object.keys(layout).find((k) => layout[k] === currentChar);
|
|
763
|
-
if (originalKey) {
|
|
764
|
-
key.textContent = originalKey;
|
|
765
|
-
key.dataset.key = originalKey;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
811
|
+
|
|
812
|
+
// FIX BUG-1: ใช้ _updateAllKeyContent แทน updateKeyContent ที่ซ้ำกัน
|
|
813
|
+
this._updateAllKeyContent(this.shiftActive);
|
|
768
814
|
}
|
|
769
815
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
816
|
+
EngAlphabetShift = {
|
|
817
|
+
"`": "~",
|
|
818
|
+
1: "!",
|
|
819
|
+
2: "@",
|
|
820
|
+
3: "#",
|
|
821
|
+
4: "$",
|
|
822
|
+
5: "%",
|
|
823
|
+
6: "^",
|
|
824
|
+
7: "&",
|
|
825
|
+
8: "*",
|
|
826
|
+
9: "(",
|
|
827
|
+
0: ")",
|
|
828
|
+
"-": "_",
|
|
829
|
+
"=": "+",
|
|
830
|
+
"[": "{",
|
|
831
|
+
"]": "}",
|
|
832
|
+
"\\": "|",
|
|
833
|
+
";": ":",
|
|
834
|
+
"'": '"',
|
|
835
|
+
",": "<",
|
|
836
|
+
".": ">",
|
|
837
|
+
"/": "?",
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
ThaiAlphabetShift = {
|
|
841
|
+
_: "%",
|
|
842
|
+
ๅ: "+",
|
|
843
|
+
"/": "๑",
|
|
844
|
+
"-": "๒",
|
|
845
|
+
ภ: "๓",
|
|
846
|
+
ถ: "๔",
|
|
847
|
+
"ุ": "ู",
|
|
848
|
+
"ึ": "฿",
|
|
849
|
+
ค: "๕",
|
|
850
|
+
ต: "๖",
|
|
851
|
+
จ: "๗",
|
|
852
|
+
ข: "๘",
|
|
853
|
+
ช: "๙",
|
|
854
|
+
ๆ: "๐",
|
|
855
|
+
ไ: '"',
|
|
856
|
+
ำ: "ฎ",
|
|
857
|
+
พ: "ฑ",
|
|
858
|
+
ะ: "ธ",
|
|
859
|
+
"ั": "ํ",
|
|
860
|
+
"ี": "๋",
|
|
861
|
+
ร: "ณ",
|
|
862
|
+
น: "ฯ",
|
|
863
|
+
ย: "ญ",
|
|
864
|
+
บ: "ฐ",
|
|
865
|
+
ล: ",",
|
|
866
|
+
ฃ: "ฅ",
|
|
867
|
+
ฟ: "ฤ",
|
|
868
|
+
ห: "ฆ",
|
|
869
|
+
ก: "ฏ",
|
|
870
|
+
ด: "โ",
|
|
871
|
+
เ: "ฌ",
|
|
872
|
+
"้": "็",
|
|
873
|
+
"่": "๋",
|
|
874
|
+
า: "ษ",
|
|
875
|
+
ส: "ศ",
|
|
876
|
+
ว: "ซ",
|
|
877
|
+
ง: ".",
|
|
878
|
+
ผ: "(",
|
|
879
|
+
ป: ")",
|
|
880
|
+
แ: "ฉ",
|
|
881
|
+
อ: "ฮ",
|
|
882
|
+
"ิ": "ฺ",
|
|
883
|
+
"ื": "์",
|
|
884
|
+
ท: "?",
|
|
885
|
+
ม: "ฒ",
|
|
886
|
+
ใ: "ฬ",
|
|
887
|
+
ฝ: "ฦ",
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
FullAlphabetShift = {
|
|
891
|
+
"[": "{",
|
|
892
|
+
"]": "}",
|
|
893
|
+
"\\": "|",
|
|
894
|
+
";": ":",
|
|
895
|
+
"'": '"',
|
|
896
|
+
",": "<",
|
|
897
|
+
".": ">",
|
|
898
|
+
"/": "?",
|
|
899
|
+
};
|
|
774
900
|
|
|
775
|
-
// [8]
|
|
776
901
|
toggle() {
|
|
777
902
|
this.isVisible = !this.isVisible;
|
|
778
903
|
this.render();
|
|
779
904
|
}
|
|
780
905
|
|
|
781
|
-
//
|
|
906
|
+
// FIX WARN-1: reset isScrambled และ shift/caps เมื่อเปลี่ยน layout
|
|
782
907
|
changeLayout(layout) {
|
|
783
908
|
this.currentLayout = layout;
|
|
909
|
+
this.isScrambled = false;
|
|
910
|
+
this.shiftActive = false;
|
|
911
|
+
this.capsLockActive = false;
|
|
784
912
|
this.render();
|
|
785
913
|
}
|
|
786
914
|
|
|
787
|
-
// [10]
|
|
788
915
|
shuffleArray(array) {
|
|
789
916
|
for (let i = array.length - 1; i > 0; i--) {
|
|
790
917
|
const j = Math.floor(Math.random() * (i + 1));
|
|
@@ -792,64 +919,74 @@ export class VirtualKeyboard {
|
|
|
792
919
|
}
|
|
793
920
|
}
|
|
794
921
|
|
|
795
|
-
//
|
|
922
|
+
// FIX BUG-7: แก้ CSS selector — เพิ่มวงเล็บปิด ) และ fix data-key case
|
|
796
923
|
scrambleKeyboard() {
|
|
797
924
|
const keys = document.querySelectorAll(
|
|
798
|
-
":not([data-key='std']):not([data-key='scr']).key:not([data-key=backspace]"
|
|
925
|
+
":not([data-key='std']):not([data-key='scr']).key:not([data-key='backspace']):not([data-key='Backspace'])",
|
|
799
926
|
);
|
|
800
927
|
const numbers = "1234567890-=+%/*.()".split("");
|
|
801
928
|
this.shuffleArray(numbers);
|
|
802
929
|
keys.forEach((key, index) => {
|
|
803
|
-
|
|
804
|
-
|
|
930
|
+
if (index < numbers.length) {
|
|
931
|
+
key.textContent = numbers[index];
|
|
932
|
+
key.dataset.key = numbers[index];
|
|
933
|
+
}
|
|
805
934
|
});
|
|
806
935
|
}
|
|
807
936
|
|
|
808
|
-
// [12]
|
|
809
937
|
scrambleEnglishKeys() {
|
|
810
938
|
const keys = document.querySelectorAll(
|
|
811
|
-
".key:not([data-key='scrambled']):not([data-key='std']):not([data-key='scr']):not([data-key=' ']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key='Tab ↹'])"
|
|
939
|
+
".key:not([data-key='scrambled']):not([data-key='std']):not([data-key='scr']):not([data-key=' ']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key='Tab ↹'])",
|
|
812
940
|
);
|
|
813
|
-
const englishAlphabet =
|
|
941
|
+
const englishAlphabet =
|
|
942
|
+
"abcdefghijklmnopqrstuvwxyz/.,';\\][`1234567890-=".split("");
|
|
814
943
|
this.shuffleArray(englishAlphabet);
|
|
815
944
|
keys.forEach((key, index) => {
|
|
816
|
-
|
|
817
|
-
|
|
945
|
+
if (index < englishAlphabet.length) {
|
|
946
|
+
key.textContent = englishAlphabet[index];
|
|
947
|
+
key.dataset.key = englishAlphabet[index];
|
|
948
|
+
}
|
|
818
949
|
});
|
|
819
950
|
}
|
|
820
951
|
|
|
821
|
-
// [13]
|
|
822
952
|
scrambleThaiKeys() {
|
|
823
953
|
const keys = document.querySelectorAll(
|
|
824
|
-
".key:not([data-key='scrambled']):not([data-key='scr']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key=' ']):not([data-key='Tab ↹'])"
|
|
954
|
+
".key:not([data-key='scrambled']):not([data-key='scr']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key=' ']):not([data-key='Tab ↹'])",
|
|
825
955
|
);
|
|
826
|
-
const ThaiAlphabet =
|
|
956
|
+
const ThaiAlphabet =
|
|
957
|
+
'_ๅ/-ภถุึคตจขชๆไำพะัีรนยบลฃฟหกดเ้่าสวงผปแอิืทมใฝ%+๑๒๓๔ู฿๕๖๗๘๙๐"ฎฑธํ๊ณฯญฐ,ฅฤฆฏโฌ็๋ษศซ.ฦฬฒ?์ฺฮฉ)('.split(
|
|
958
|
+
"",
|
|
959
|
+
);
|
|
827
960
|
this.shuffleArray(ThaiAlphabet);
|
|
828
961
|
keys.forEach((key, index) => {
|
|
829
|
-
|
|
830
|
-
|
|
962
|
+
if (index < ThaiAlphabet.length) {
|
|
963
|
+
key.textContent = ThaiAlphabet[index];
|
|
964
|
+
key.dataset.key = ThaiAlphabet[index];
|
|
965
|
+
}
|
|
831
966
|
});
|
|
832
967
|
}
|
|
833
968
|
|
|
834
|
-
//
|
|
969
|
+
// FIX BUG-7: แก้ CSS selector ของ scrambleSymbols
|
|
835
970
|
scrambleSymbols() {
|
|
836
971
|
const keys = document.querySelectorAll(
|
|
837
|
-
":not([data-key='std']):not([data-key='scr']).key:not([data-key=backspace]"
|
|
972
|
+
":not([data-key='std']):not([data-key='scr']).key:not([data-key='backspace']):not([data-key='Backspace'])",
|
|
838
973
|
);
|
|
839
|
-
const
|
|
840
|
-
this.shuffleArray(
|
|
974
|
+
const symbols = "@#$%^&*()_+~`{}|\\:'<>?/[]±§¶!€£¥¢©®™℅‰†".split("");
|
|
975
|
+
this.shuffleArray(symbols);
|
|
841
976
|
keys.forEach((key, index) => {
|
|
842
|
-
|
|
843
|
-
|
|
977
|
+
if (index < symbols.length) {
|
|
978
|
+
key.textContent = symbols[index];
|
|
979
|
+
key.dataset.key = symbols[index];
|
|
980
|
+
}
|
|
844
981
|
});
|
|
845
982
|
}
|
|
846
983
|
|
|
847
|
-
|
|
848
984
|
scrambleFullLayoutKeys() {
|
|
849
985
|
const keys = document.querySelectorAll(
|
|
850
|
-
".key:not(.concat-keys):not([data-key='scrambled']):not([data-key='std']):not([data-key='scr']):not([data-key=' ']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key='Tab ↹'])"
|
|
986
|
+
".key:not(.concat-keys):not([data-key='scrambled']):not([data-key='std']):not([data-key='scr']):not([data-key=' ']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key='Tab ↹'])",
|
|
851
987
|
);
|
|
852
|
-
const englishChars =
|
|
988
|
+
const englishChars =
|
|
989
|
+
"abcdefghijklmnopqrstuvwxyz1234567890;'\\/][`,.-=".split("");
|
|
853
990
|
this.shuffleArray(englishChars);
|
|
854
991
|
keys.forEach((key, index) => {
|
|
855
992
|
if (index < englishChars.length) {
|
|
@@ -859,20 +996,24 @@ export class VirtualKeyboard {
|
|
|
859
996
|
});
|
|
860
997
|
}
|
|
861
998
|
|
|
862
|
-
//
|
|
999
|
+
// FIX BUG-2: เก็บ bound reference และ cleanup ให้ถูกต้อง
|
|
863
1000
|
startDrag(event) {
|
|
864
1001
|
this.isDragging = true;
|
|
865
|
-
this.offsetX =
|
|
866
|
-
|
|
1002
|
+
this.offsetX =
|
|
1003
|
+
event.clientX - document.getElementById("keyboard").offsetLeft;
|
|
1004
|
+
this.offsetY =
|
|
1005
|
+
event.clientY - document.getElementById("keyboard").offsetTop;
|
|
867
1006
|
|
|
868
|
-
document.addEventListener("mousemove", this.
|
|
869
|
-
document.addEventListener("mouseup",
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1007
|
+
document.addEventListener("mousemove", this._boundDrag);
|
|
1008
|
+
document.addEventListener("mouseup", this._boundStopDrag);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
stopDrag() {
|
|
1012
|
+
this.isDragging = false;
|
|
1013
|
+
document.removeEventListener("mousemove", this._boundDrag);
|
|
1014
|
+
document.removeEventListener("mouseup", this._boundStopDrag);
|
|
873
1015
|
}
|
|
874
1016
|
|
|
875
|
-
// [15]
|
|
876
1017
|
drag(event) {
|
|
877
1018
|
if (this.isDragging) {
|
|
878
1019
|
const keyboard = document.getElementById("keyboard");
|
|
@@ -880,4 +1021,4 @@ export class VirtualKeyboard {
|
|
|
880
1021
|
keyboard.style.top = `${event.clientY - this.offsetY}px`;
|
|
881
1022
|
}
|
|
882
1023
|
}
|
|
883
|
-
}
|
|
1024
|
+
}
|