js_lis 2.0.7 → 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 +552 -302
- 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,30 +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
|
-
this.handleRealKeyboardInput(target);
|
|
162
|
+
this.handleRealKeyboardInput(target, e);
|
|
83
163
|
}
|
|
84
164
|
}
|
|
85
165
|
});
|
|
86
166
|
|
|
87
|
-
// Add real keyboard arrow key support
|
|
88
167
|
document.addEventListener("keydown", (e) => {
|
|
89
168
|
const target = e.target;
|
|
90
|
-
if (
|
|
91
|
-
|
|
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)) {
|
|
183
|
+
this.isVirtualKeyboardActive = false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!this.isVirtualKeyboardActive) {
|
|
187
|
+
this.handleRealKeyboardKeydown(target, e);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (navKeys.includes(e.code)) {
|
|
92
191
|
setTimeout(() => {
|
|
93
192
|
target.focus();
|
|
94
193
|
}, 0);
|
|
@@ -109,29 +208,14 @@ export class VirtualKeyboard {
|
|
|
109
208
|
|
|
110
209
|
this.currentInput = inputElement;
|
|
111
210
|
this.currentInput.classList.add("keyboard-active");
|
|
112
|
-
|
|
113
211
|
this.currentInput.focus();
|
|
114
|
-
|
|
115
|
-
// Ensure buffer initialized for this input
|
|
212
|
+
|
|
116
213
|
if (!this.inputPlaintextBuffers.has(this.currentInput)) {
|
|
117
|
-
const
|
|
118
|
-
let initial = "";
|
|
119
|
-
try {
|
|
120
|
-
if (ds && window.VKCrypto && typeof window.VKCrypto.decrypt === "function") {
|
|
121
|
-
initial = window.VKCrypto.decrypt(ds) || "";
|
|
122
|
-
} else {
|
|
123
|
-
initial = this.currentInput.value || "";
|
|
124
|
-
}
|
|
125
|
-
} catch (_) {
|
|
126
|
-
initial = this.currentInput.value || "";
|
|
127
|
-
}
|
|
214
|
+
const initial = this.currentInput.value || "";
|
|
128
215
|
this.inputPlaintextBuffers.set(this.currentInput, initial);
|
|
129
|
-
this.updateDomEncrypted(this.currentInput, initial);
|
|
130
216
|
}
|
|
131
|
-
// Sync visible with buffer (masking for password)
|
|
132
217
|
this.updateDisplayedValue();
|
|
133
|
-
|
|
134
|
-
// Only move cursor to end on initial focus, not during virtual keyboard operations
|
|
218
|
+
|
|
135
219
|
if (moveToEnd) {
|
|
136
220
|
setTimeout(() => {
|
|
137
221
|
const length = this.currentInput.value.length;
|
|
@@ -140,7 +224,6 @@ export class VirtualKeyboard {
|
|
|
140
224
|
}
|
|
141
225
|
}
|
|
142
226
|
|
|
143
|
-
// [1]
|
|
144
227
|
render() {
|
|
145
228
|
const keyboard = document.createElement("div");
|
|
146
229
|
keyboard.className = `virtual-keyboard ${this.currentLayout}`;
|
|
@@ -156,10 +239,11 @@ export class VirtualKeyboard {
|
|
|
156
239
|
|
|
157
240
|
const layoutSelector = document.createElement("select");
|
|
158
241
|
layoutSelector.id = "layout-selector";
|
|
242
|
+
layoutSelector.setAttribute("aria-label", "Select keyboard layout");
|
|
159
243
|
layoutSelector.onchange = (e) => this.changeLayout(e.target.value);
|
|
160
244
|
|
|
161
|
-
const
|
|
162
|
-
|
|
245
|
+
const availableLayouts = ["full", "en", "th", "numpad", "symbols"];
|
|
246
|
+
availableLayouts.forEach((layout) => {
|
|
163
247
|
const option = document.createElement("option");
|
|
164
248
|
option.value = layout;
|
|
165
249
|
option.innerText = this.getLayoutName(layout);
|
|
@@ -181,15 +265,25 @@ export class VirtualKeyboard {
|
|
|
181
265
|
keyElement.className = "keyboard-key key";
|
|
182
266
|
keyElement.textContent = key;
|
|
183
267
|
keyElement.type = "button";
|
|
184
|
-
|
|
185
268
|
keyElement.dataset.key = key;
|
|
186
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
|
+
|
|
187
280
|
if (index >= row.length - 4) {
|
|
188
281
|
keyElement.classList.add("concat-keys");
|
|
189
282
|
}
|
|
190
283
|
|
|
191
|
-
if (key === "
|
|
192
|
-
keyElement.className += "
|
|
284
|
+
if (key === " ") {
|
|
285
|
+
keyElement.className += " spacebar";
|
|
286
|
+
keyElement.innerHTML = "";
|
|
193
287
|
}
|
|
194
288
|
|
|
195
289
|
if (key === "backspace" || key === "Backspace") {
|
|
@@ -197,6 +291,14 @@ export class VirtualKeyboard {
|
|
|
197
291
|
keyElement.innerHTML = '<i class="fa fa-backspace"></i>';
|
|
198
292
|
}
|
|
199
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");
|
|
300
|
+
}
|
|
301
|
+
|
|
200
302
|
keyElement.onclick = (e) => {
|
|
201
303
|
e.preventDefault();
|
|
202
304
|
const keyPressed = keyElement.dataset.key || keyElement.textContent;
|
|
@@ -216,16 +318,16 @@ export class VirtualKeyboard {
|
|
|
216
318
|
this.container.innerHTML = "";
|
|
217
319
|
this.container.appendChild(keyboard);
|
|
218
320
|
|
|
321
|
+
// FIX BUG-2: ใช้ bound reference แทน bind() inline
|
|
219
322
|
keyboard.addEventListener("mousedown", (event) => this.startDrag(event));
|
|
220
323
|
}
|
|
221
324
|
|
|
222
|
-
//
|
|
223
|
-
|
|
325
|
+
// FIX BUG-3 & WARN-5: เปลี่ยนเป็น sync, จัดการ isVirtualKeyboardActive อย่างถูกต้อง
|
|
326
|
+
handleKeyPress(keyPressed) {
|
|
224
327
|
if (!this.currentInput) return;
|
|
225
|
-
|
|
226
|
-
// Set flag to indicate virtual keyboard is active
|
|
328
|
+
|
|
227
329
|
this.isVirtualKeyboardActive = true;
|
|
228
|
-
|
|
330
|
+
|
|
229
331
|
const start = this.currentInput.selectionStart;
|
|
230
332
|
const end = this.currentInput.selectionEnd;
|
|
231
333
|
const buffer = this.getCurrentBuffer();
|
|
@@ -234,9 +336,7 @@ export class VirtualKeyboard {
|
|
|
234
336
|
const isShiftActive = this.shiftActive;
|
|
235
337
|
|
|
236
338
|
const convertToCorrectCase = (char) => {
|
|
237
|
-
if (isCapsActive || isShiftActive)
|
|
238
|
-
return char.toUpperCase();
|
|
239
|
-
}
|
|
339
|
+
if (isCapsActive || isShiftActive) return char.toUpperCase();
|
|
240
340
|
return char.toLowerCase();
|
|
241
341
|
};
|
|
242
342
|
|
|
@@ -246,71 +346,32 @@ export class VirtualKeyboard {
|
|
|
246
346
|
}
|
|
247
347
|
|
|
248
348
|
if (keyPressed === "scr" || keyPressed === "scrambled") {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.unscrambleKeys()
|
|
252
|
-
|
|
253
|
-
this.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
this.scrambleThaiKeys();
|
|
268
|
-
}
|
|
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();
|
|
269
367
|
this.isScrambled = !this.isScrambled;
|
|
270
368
|
document
|
|
271
|
-
.querySelectorAll('.key[data-key="scrambled"]')
|
|
369
|
+
.querySelectorAll('.key[data-key="scr"], .key[data-key="scrambled"]')
|
|
272
370
|
.forEach((key) => {
|
|
273
371
|
key.classList.toggle("active", this.isScrambled);
|
|
274
372
|
});
|
|
275
373
|
}
|
|
276
374
|
|
|
277
|
-
if (this.currentLayout === "numpad") {
|
|
278
|
-
if (this.isScrambled) {
|
|
279
|
-
this.unscrambleKeys();
|
|
280
|
-
} else {
|
|
281
|
-
this.scrambleKeyboard();
|
|
282
|
-
}
|
|
283
|
-
this.isScrambled = !this.isScrambled;
|
|
284
|
-
document.querySelectorAll('.key[data-key="scr"]').forEach((key) => {
|
|
285
|
-
key.classList.toggle("active", this.isScrambled);
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (this.currentLayout === "symbols") {
|
|
290
|
-
if (this.isScrambled) {
|
|
291
|
-
this.unscrambleKeys();
|
|
292
|
-
} else {
|
|
293
|
-
this.scrambleSymbols();
|
|
294
|
-
}
|
|
295
|
-
this.isScrambled = !this.isScrambled;
|
|
296
|
-
document.querySelectorAll('.key[data-key="scr"]').forEach((key) => {
|
|
297
|
-
key.classList.toggle("active", this.isScrambled);
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (this.currentLayout === "full") {
|
|
302
|
-
if (this.isScrambled) {
|
|
303
|
-
this.unscrambleKeys();
|
|
304
|
-
} else {
|
|
305
|
-
this.scrambleFullLayoutKeys();
|
|
306
|
-
}
|
|
307
|
-
this.isScrambled = !this.isScrambled;
|
|
308
|
-
document
|
|
309
|
-
.querySelectorAll('.key[data-key="scrambled"]')
|
|
310
|
-
.forEach((key) => {
|
|
311
|
-
key.classList.toggle("active", this.isScrambled);
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
375
|
this.isVirtualKeyboardActive = false;
|
|
315
376
|
return;
|
|
316
377
|
}
|
|
@@ -323,11 +384,10 @@ export class VirtualKeyboard {
|
|
|
323
384
|
this.currentInput.setSelectionRange(0, 0);
|
|
324
385
|
this.currentInput.focus();
|
|
325
386
|
}, 5);
|
|
326
|
-
// Skip the general cursor repositioning for navigation keys
|
|
327
387
|
this.isVirtualKeyboardActive = false;
|
|
328
388
|
return;
|
|
329
389
|
|
|
330
|
-
case "END":
|
|
390
|
+
case "END": {
|
|
331
391
|
const bufferLengthEnd = this.getCurrentBuffer().length;
|
|
332
392
|
this.currentInput.setSelectionRange(bufferLengthEnd, bufferLengthEnd);
|
|
333
393
|
this.currentInput.focus();
|
|
@@ -335,9 +395,9 @@ export class VirtualKeyboard {
|
|
|
335
395
|
this.currentInput.setSelectionRange(bufferLengthEnd, bufferLengthEnd);
|
|
336
396
|
this.currentInput.focus();
|
|
337
397
|
}, 5);
|
|
338
|
-
// Skip the general cursor repositioning for navigation keys
|
|
339
398
|
this.isVirtualKeyboardActive = false;
|
|
340
399
|
return;
|
|
400
|
+
}
|
|
341
401
|
|
|
342
402
|
case "Backspace":
|
|
343
403
|
case "backspace":
|
|
@@ -347,12 +407,11 @@ export class VirtualKeyboard {
|
|
|
347
407
|
const newCursorPos = start - 1;
|
|
348
408
|
this.updateDisplayedValue(false);
|
|
349
409
|
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
350
|
-
} else {
|
|
410
|
+
} else if (start !== end) {
|
|
351
411
|
const newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
352
412
|
this.setCurrentBuffer(newBuffer);
|
|
353
|
-
const newCursorPos = start;
|
|
354
413
|
this.updateDisplayedValue(false);
|
|
355
|
-
this.currentInput.setSelectionRange(
|
|
414
|
+
this.currentInput.setSelectionRange(start, start);
|
|
356
415
|
}
|
|
357
416
|
break;
|
|
358
417
|
|
|
@@ -360,24 +419,22 @@ export class VirtualKeyboard {
|
|
|
360
419
|
if (start === end && start < buffer.length) {
|
|
361
420
|
const newBuffer = buffer.slice(0, start) + buffer.slice(end + 1);
|
|
362
421
|
this.setCurrentBuffer(newBuffer);
|
|
363
|
-
const newCursorPos = start;
|
|
364
422
|
this.updateDisplayedValue(false);
|
|
365
|
-
this.currentInput.setSelectionRange(
|
|
366
|
-
} else {
|
|
423
|
+
this.currentInput.setSelectionRange(start, start);
|
|
424
|
+
} else if (start !== end) {
|
|
367
425
|
const newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
368
426
|
this.setCurrentBuffer(newBuffer);
|
|
369
|
-
const newCursorPos = start;
|
|
370
427
|
this.updateDisplayedValue(false);
|
|
371
|
-
this.currentInput.setSelectionRange(
|
|
428
|
+
this.currentInput.setSelectionRange(start, start);
|
|
372
429
|
}
|
|
373
430
|
break;
|
|
374
431
|
|
|
375
432
|
case "Space":
|
|
376
|
-
|
|
433
|
+
this.insertText(" ");
|
|
377
434
|
break;
|
|
378
435
|
|
|
379
436
|
case "Tab ↹":
|
|
380
|
-
|
|
437
|
+
this.insertText("\t");
|
|
381
438
|
break;
|
|
382
439
|
|
|
383
440
|
case "Enter":
|
|
@@ -387,12 +444,17 @@ export class VirtualKeyboard {
|
|
|
387
444
|
const newCursorPos = start + 1;
|
|
388
445
|
this.updateDisplayedValue(false);
|
|
389
446
|
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
390
|
-
} else if (
|
|
447
|
+
} else if (
|
|
448
|
+
this.currentInput.tagName === "INPUT" ||
|
|
449
|
+
this.currentInput.type === "password" ||
|
|
450
|
+
this.currentInput.type === "text"
|
|
451
|
+
) {
|
|
391
452
|
if (this.currentInput.form) {
|
|
392
453
|
const submitButton = this.currentInput.form.querySelector(
|
|
393
|
-
'input[type="submit"], button[type="submit"], button[type="button"], button[onclick]'
|
|
454
|
+
'input[type="submit"], button[type="submit"], button[type="button"], button[onclick]',
|
|
394
455
|
);
|
|
395
|
-
if (submitButton) submitButton.click();
|
|
456
|
+
if (submitButton) submitButton.click();
|
|
457
|
+
else this.currentInput.form.submit();
|
|
396
458
|
} else {
|
|
397
459
|
const newBuffer = buffer + "\n";
|
|
398
460
|
this.setCurrentBuffer(newBuffer);
|
|
@@ -409,273 +471,447 @@ export class VirtualKeyboard {
|
|
|
409
471
|
this.toggleShift();
|
|
410
472
|
break;
|
|
411
473
|
|
|
412
|
-
case "←":
|
|
474
|
+
case "←": {
|
|
413
475
|
const leftNewPos = Math.max(0, start - 1);
|
|
414
476
|
this.currentInput.setSelectionRange(leftNewPos, leftNewPos);
|
|
415
|
-
|
|
477
|
+
this.isVirtualKeyboardActive = false;
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
416
480
|
|
|
417
|
-
case "→":
|
|
481
|
+
case "→": {
|
|
418
482
|
const bufferLength = this.getCurrentBuffer().length;
|
|
419
483
|
const rightNewPos = Math.min(bufferLength, start + 1);
|
|
420
484
|
this.currentInput.setSelectionRange(rightNewPos, rightNewPos);
|
|
421
|
-
|
|
485
|
+
this.isVirtualKeyboardActive = false;
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
422
488
|
|
|
423
489
|
case "↑":
|
|
424
|
-
case "↓":
|
|
425
|
-
// Use the actual buffer content instead of displayed value
|
|
490
|
+
case "↓": {
|
|
426
491
|
const actualText = this.getCurrentBuffer();
|
|
427
492
|
const actualLines = actualText.substring(0, start).split("\n");
|
|
428
493
|
const actualCurrentLineIndex = actualLines.length - 1;
|
|
429
494
|
const actualCurrentLine = actualLines[actualCurrentLineIndex];
|
|
430
|
-
const actualColumnIndex =
|
|
495
|
+
const actualColumnIndex =
|
|
496
|
+
start - actualText.lastIndexOf("\n", start - 1) - 1;
|
|
431
497
|
|
|
432
498
|
if (keyPressed === "↑" && actualCurrentLineIndex > 0) {
|
|
433
|
-
// Move to previous line
|
|
434
499
|
const prevLineLength = actualLines[actualCurrentLineIndex - 1].length;
|
|
435
|
-
const upNewPos =
|
|
436
|
-
|
|
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
|
+
);
|
|
437
509
|
} else if (keyPressed === "↓") {
|
|
438
|
-
// Move to next line
|
|
439
510
|
const remainingText = actualText.substring(start);
|
|
440
511
|
const nextLineBreak = remainingText.indexOf("\n");
|
|
441
|
-
|
|
442
512
|
if (nextLineBreak !== -1) {
|
|
443
|
-
|
|
444
|
-
|
|
513
|
+
const textAfterNextLineBreak = remainingText.substring(
|
|
514
|
+
nextLineBreak + 1,
|
|
515
|
+
);
|
|
445
516
|
const nextLineEnd = textAfterNextLineBreak.indexOf("\n");
|
|
446
|
-
const nextLineLength =
|
|
447
|
-
|
|
448
|
-
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);
|
|
449
524
|
this.currentInput.setSelectionRange(downNewPos, downNewPos);
|
|
450
525
|
}
|
|
451
|
-
// If no next line, do nothing (cursor stays at current position)
|
|
452
526
|
}
|
|
453
|
-
|
|
527
|
+
|
|
454
528
|
this.currentInput.focus();
|
|
455
|
-
// Skip the general cursor repositioning for arrow keys
|
|
456
529
|
this.isVirtualKeyboardActive = false;
|
|
457
|
-
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
458
532
|
|
|
459
|
-
default:
|
|
460
|
-
const textvalue =
|
|
461
|
-
|
|
533
|
+
default: {
|
|
534
|
+
const textvalue = convertToCorrectCase(keyPressed);
|
|
535
|
+
this.insertText(textvalue);
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
462
538
|
}
|
|
463
539
|
|
|
464
540
|
if (isShiftActive && !isCapsActive) this.toggleShift();
|
|
465
541
|
|
|
466
542
|
this.currentInput.focus();
|
|
467
543
|
|
|
544
|
+
// FIX BUG-3: reset flag หลัง setTimeout เพื่อป้องกัน real keyboard listener เข้ามาแทรก
|
|
468
545
|
setTimeout(() => {
|
|
469
|
-
this.currentInput
|
|
470
|
-
|
|
471
|
-
|
|
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;
|
|
472
552
|
}, 0);
|
|
473
|
-
|
|
553
|
+
|
|
474
554
|
this.currentInput.focus();
|
|
475
555
|
const event = new Event("input", { bubbles: true });
|
|
476
556
|
this.currentInput.dispatchEvent(event);
|
|
477
557
|
}
|
|
478
558
|
|
|
479
|
-
|
|
480
|
-
async insertText(text) {
|
|
559
|
+
insertText(text) {
|
|
481
560
|
const start = this.currentInput.selectionStart;
|
|
482
561
|
const end = this.currentInput.selectionEnd;
|
|
483
|
-
const textvalue = text;
|
|
484
562
|
|
|
485
563
|
const buffer = this.getCurrentBuffer();
|
|
486
|
-
const newBuffer = buffer.slice(0, start) +
|
|
564
|
+
const newBuffer = buffer.slice(0, start) + text + buffer.slice(end);
|
|
487
565
|
this.setCurrentBuffer(newBuffer);
|
|
488
|
-
|
|
489
|
-
const newCursorPos = start +
|
|
490
|
-
|
|
566
|
+
|
|
567
|
+
const newCursorPos = start + text.length;
|
|
491
568
|
this.updateDisplayedValue(false);
|
|
492
|
-
|
|
493
569
|
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
494
570
|
}
|
|
495
571
|
|
|
496
|
-
// Helper: get current plaintext buffer for active input
|
|
497
572
|
getCurrentBuffer() {
|
|
498
573
|
if (!this.currentInput) return "";
|
|
499
574
|
return this.inputPlaintextBuffers.get(this.currentInput) || "";
|
|
500
575
|
}
|
|
501
576
|
|
|
502
|
-
|
|
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
|
+
|
|
503
586
|
setCurrentBuffer(newBuffer) {
|
|
504
587
|
if (!this.currentInput) return;
|
|
505
588
|
this.inputPlaintextBuffers.set(this.currentInput, newBuffer);
|
|
506
|
-
this.updateDomEncrypted(this.currentInput, newBuffer);
|
|
507
589
|
}
|
|
508
590
|
|
|
509
|
-
// Helper: reflect masked value in the visible input/textarea
|
|
510
591
|
updateDisplayedValue(preserveCursor = true) {
|
|
511
592
|
if (!this.currentInput) return;
|
|
512
593
|
const buffer = this.getCurrentBuffer();
|
|
513
594
|
const isPassword = this.currentInput.type === "password";
|
|
514
595
|
const maskChar = "•";
|
|
515
|
-
|
|
516
|
-
// Store current cursor position only if we want to preserve it
|
|
596
|
+
|
|
517
597
|
const currentStart = preserveCursor ? this.currentInput.selectionStart : 0;
|
|
518
598
|
const currentEnd = preserveCursor ? this.currentInput.selectionEnd : 0;
|
|
519
|
-
|
|
520
|
-
this.currentInput.value = isPassword
|
|
521
|
-
|
|
522
|
-
|
|
599
|
+
|
|
600
|
+
this.currentInput.value = isPassword
|
|
601
|
+
? maskChar.repeat(buffer.length)
|
|
602
|
+
: buffer;
|
|
603
|
+
|
|
523
604
|
if (preserveCursor) {
|
|
524
605
|
this.currentInput.setSelectionRange(currentStart, currentEnd);
|
|
525
606
|
}
|
|
526
607
|
}
|
|
527
608
|
|
|
528
|
-
|
|
529
|
-
handleRealKeyboardInput(input) {
|
|
609
|
+
handleRealKeyboardKeydown(input, event) {
|
|
530
610
|
if (!input) return;
|
|
531
|
-
|
|
532
|
-
// Get the current value from the input
|
|
533
|
-
const currentValue = input.value;
|
|
534
|
-
|
|
535
|
-
// Update the plaintext buffer with the real keyboard input
|
|
536
|
-
this.inputPlaintextBuffers.set(input, currentValue);
|
|
537
|
-
|
|
538
|
-
// Encrypt and store in data-encrypted attribute
|
|
539
|
-
this.updateDomEncrypted(input, currentValue);
|
|
540
|
-
}
|
|
541
611
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
612
|
+
const key = event.key;
|
|
613
|
+
const start = input.selectionStart;
|
|
614
|
+
const end = input.selectionEnd;
|
|
615
|
+
const buffer = this.getCurrentBuffer();
|
|
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
|
+
) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
let newBuffer = buffer;
|
|
660
|
+
|
|
661
|
+
if (key === "Backspace") {
|
|
662
|
+
if (start === end && start > 0) {
|
|
663
|
+
newBuffer = buffer.slice(0, start - 1) + buffer.slice(start);
|
|
664
|
+
} else if (start !== end) {
|
|
665
|
+
newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
event.preventDefault();
|
|
669
|
+
this.setCurrentBuffer(newBuffer);
|
|
670
|
+
this.updateDisplayedValue(false);
|
|
671
|
+
|
|
672
|
+
const newCursorPos = start === end ? Math.max(0, start - 1) : start;
|
|
673
|
+
setTimeout(() => {
|
|
674
|
+
input.setSelectionRange(newCursorPos, newCursorPos);
|
|
675
|
+
input.focus();
|
|
676
|
+
}, 0);
|
|
677
|
+
} else if (key === "Delete") {
|
|
678
|
+
if (start === end && start < buffer.length) {
|
|
679
|
+
newBuffer = buffer.slice(0, start) + buffer.slice(start + 1);
|
|
680
|
+
} else if (start !== end) {
|
|
681
|
+
newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
event.preventDefault();
|
|
685
|
+
this.setCurrentBuffer(newBuffer);
|
|
686
|
+
this.updateDisplayedValue(false);
|
|
687
|
+
|
|
688
|
+
setTimeout(() => {
|
|
689
|
+
input.setSelectionRange(start, start);
|
|
690
|
+
input.focus();
|
|
691
|
+
}, 0);
|
|
692
|
+
} else if (key === "Enter") {
|
|
693
|
+
if (input.tagName === "TEXTAREA") {
|
|
694
|
+
newBuffer = buffer.slice(0, start) + "\n" + buffer.slice(end);
|
|
695
|
+
|
|
696
|
+
event.preventDefault();
|
|
697
|
+
this.setCurrentBuffer(newBuffer);
|
|
698
|
+
this.updateDisplayedValue(false);
|
|
699
|
+
|
|
700
|
+
const newCursorPos = start + 1;
|
|
701
|
+
setTimeout(() => {
|
|
702
|
+
input.setSelectionRange(newCursorPos, newCursorPos);
|
|
703
|
+
input.focus();
|
|
704
|
+
}, 0);
|
|
554
705
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
706
|
+
} else if (key.length === 1) {
|
|
707
|
+
newBuffer = buffer.slice(0, start) + key + buffer.slice(end);
|
|
708
|
+
|
|
709
|
+
event.preventDefault();
|
|
710
|
+
this.setCurrentBuffer(newBuffer);
|
|
711
|
+
this.updateDisplayedValue(false);
|
|
712
|
+
|
|
713
|
+
const newCursorPos = start + key.length;
|
|
714
|
+
setTimeout(() => {
|
|
715
|
+
input.setSelectionRange(newCursorPos, newCursorPos);
|
|
716
|
+
input.focus();
|
|
717
|
+
}, 0);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// FIX BUG-4: handleRealKeyboardInput ทำหน้าที่ sync buffer เมื่อ autocomplete / IME
|
|
722
|
+
handleRealKeyboardInput(input, event) {
|
|
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
|
+
}
|
|
734
|
+
|
|
735
|
+
// IME composition end
|
|
736
|
+
if (
|
|
737
|
+
event.isComposing === false &&
|
|
738
|
+
event.inputType === "insertCompositionText"
|
|
739
|
+
) {
|
|
740
|
+
this.inputPlaintextBuffers.set(input, input.value);
|
|
559
741
|
}
|
|
560
742
|
}
|
|
561
743
|
|
|
562
|
-
// [4]
|
|
563
744
|
unscrambleKeys() {
|
|
564
|
-
this.keys = this.originalKeys;
|
|
565
745
|
this.render();
|
|
566
746
|
}
|
|
567
747
|
|
|
568
|
-
//
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
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];
|
|
576
757
|
|
|
577
758
|
document.querySelectorAll(".key").forEach((key) => {
|
|
578
|
-
const isLetter =
|
|
759
|
+
const isLetter =
|
|
760
|
+
key.dataset.key &&
|
|
761
|
+
key.dataset.key.length === 1 &&
|
|
762
|
+
/[a-zA-Zก-๙]/.test(key.dataset.key);
|
|
579
763
|
|
|
580
764
|
if (isLetter) {
|
|
581
|
-
key.textContent =
|
|
765
|
+
key.textContent = isActive
|
|
582
766
|
? key.dataset.key.toUpperCase()
|
|
583
767
|
: key.dataset.key.toLowerCase();
|
|
584
768
|
}
|
|
585
|
-
|
|
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
|
+
}
|
|
586
785
|
});
|
|
587
786
|
}
|
|
588
|
-
updateKeyContent(key, capsLockActive) {
|
|
589
|
-
const currentChar = key.textContent.trim();
|
|
590
787
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
if (capsLockActive && layout[currentChar]) {
|
|
602
|
-
key.textContent = layout[currentChar];
|
|
603
|
-
key.dataset.key = layout[currentChar];
|
|
604
|
-
} else if (!capsLockActive && Object.values(layout).includes(currentChar)) {
|
|
605
|
-
const originalKey = Object.keys(layout).find((k) => layout[k] === currentChar);
|
|
606
|
-
if (originalKey) {
|
|
607
|
-
key.textContent = originalKey;
|
|
608
|
-
key.dataset.key = originalKey;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
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);
|
|
611
798
|
}
|
|
612
799
|
|
|
613
|
-
// [6]
|
|
614
800
|
toggleShift() {
|
|
615
801
|
if (this.capsLockActive) {
|
|
616
802
|
return this.toggleCapsLock();
|
|
617
803
|
}
|
|
618
804
|
|
|
619
805
|
this.shiftActive = !this.shiftActive;
|
|
806
|
+
|
|
620
807
|
document.querySelectorAll('.key[data-key="Shift ⇧"]').forEach((key) => {
|
|
621
808
|
key.classList.toggle("active", this.shiftActive);
|
|
622
809
|
key.classList.toggle("bg-gray-400", this.shiftActive);
|
|
623
810
|
});
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (isLetter) {
|
|
629
|
-
key.textContent = this.shiftActive
|
|
630
|
-
? key.dataset.key.toUpperCase()
|
|
631
|
-
: key.dataset.key.toLowerCase();
|
|
632
|
-
}
|
|
633
|
-
this.updateKeyContent(key, this.shiftActive);
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
updateKeyContent(key, shiftActive) {
|
|
637
|
-
const currentChar = key.textContent.trim();
|
|
638
|
-
|
|
639
|
-
const layouts = {
|
|
640
|
-
th: this.ThaiAlphabetShift,
|
|
641
|
-
en: this.EngAlphabetShift,
|
|
642
|
-
enSc: this.EngAlphabetShift,
|
|
643
|
-
full: this.FullAlphabetShift,
|
|
644
|
-
};
|
|
645
|
-
const layout = layouts[this.currentLayout];
|
|
646
|
-
|
|
647
|
-
if (!layout) return;
|
|
648
|
-
|
|
649
|
-
if (shiftActive && layout[currentChar]) {
|
|
650
|
-
key.textContent = layout[currentChar];
|
|
651
|
-
key.dataset.key = layout[currentChar];
|
|
652
|
-
} else if (!shiftActive && Object.values(layout).includes(currentChar)) {
|
|
653
|
-
const originalKey = Object.keys(layout).find((k) => layout[k] === currentChar);
|
|
654
|
-
if (originalKey) {
|
|
655
|
-
key.textContent = originalKey;
|
|
656
|
-
key.dataset.key = originalKey;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
811
|
+
|
|
812
|
+
// FIX BUG-1: ใช้ _updateAllKeyContent แทน updateKeyContent ที่ซ้ำกัน
|
|
813
|
+
this._updateAllKeyContent(this.shiftActive);
|
|
659
814
|
}
|
|
660
815
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
+
};
|
|
665
900
|
|
|
666
|
-
// [8]
|
|
667
901
|
toggle() {
|
|
668
902
|
this.isVisible = !this.isVisible;
|
|
669
903
|
this.render();
|
|
670
904
|
}
|
|
671
905
|
|
|
672
|
-
//
|
|
906
|
+
// FIX WARN-1: reset isScrambled และ shift/caps เมื่อเปลี่ยน layout
|
|
673
907
|
changeLayout(layout) {
|
|
674
908
|
this.currentLayout = layout;
|
|
909
|
+
this.isScrambled = false;
|
|
910
|
+
this.shiftActive = false;
|
|
911
|
+
this.capsLockActive = false;
|
|
675
912
|
this.render();
|
|
676
913
|
}
|
|
677
914
|
|
|
678
|
-
// [10]
|
|
679
915
|
shuffleArray(array) {
|
|
680
916
|
for (let i = array.length - 1; i > 0; i--) {
|
|
681
917
|
const j = Math.floor(Math.random() * (i + 1));
|
|
@@ -683,64 +919,74 @@ export class VirtualKeyboard {
|
|
|
683
919
|
}
|
|
684
920
|
}
|
|
685
921
|
|
|
686
|
-
//
|
|
922
|
+
// FIX BUG-7: แก้ CSS selector — เพิ่มวงเล็บปิด ) และ fix data-key case
|
|
687
923
|
scrambleKeyboard() {
|
|
688
924
|
const keys = document.querySelectorAll(
|
|
689
|
-
":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'])",
|
|
690
926
|
);
|
|
691
927
|
const numbers = "1234567890-=+%/*.()".split("");
|
|
692
928
|
this.shuffleArray(numbers);
|
|
693
929
|
keys.forEach((key, index) => {
|
|
694
|
-
|
|
695
|
-
|
|
930
|
+
if (index < numbers.length) {
|
|
931
|
+
key.textContent = numbers[index];
|
|
932
|
+
key.dataset.key = numbers[index];
|
|
933
|
+
}
|
|
696
934
|
});
|
|
697
935
|
}
|
|
698
936
|
|
|
699
|
-
// [12]
|
|
700
937
|
scrambleEnglishKeys() {
|
|
701
938
|
const keys = document.querySelectorAll(
|
|
702
|
-
".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 ↹'])",
|
|
703
940
|
);
|
|
704
|
-
const englishAlphabet =
|
|
941
|
+
const englishAlphabet =
|
|
942
|
+
"abcdefghijklmnopqrstuvwxyz/.,';\\][`1234567890-=".split("");
|
|
705
943
|
this.shuffleArray(englishAlphabet);
|
|
706
944
|
keys.forEach((key, index) => {
|
|
707
|
-
|
|
708
|
-
|
|
945
|
+
if (index < englishAlphabet.length) {
|
|
946
|
+
key.textContent = englishAlphabet[index];
|
|
947
|
+
key.dataset.key = englishAlphabet[index];
|
|
948
|
+
}
|
|
709
949
|
});
|
|
710
950
|
}
|
|
711
951
|
|
|
712
|
-
// [13]
|
|
713
952
|
scrambleThaiKeys() {
|
|
714
953
|
const keys = document.querySelectorAll(
|
|
715
|
-
".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 ↹'])",
|
|
716
955
|
);
|
|
717
|
-
const ThaiAlphabet =
|
|
956
|
+
const ThaiAlphabet =
|
|
957
|
+
'_ๅ/-ภถุึคตจขชๆไำพะัีรนยบลฃฟหกดเ้่าสวงผปแอิืทมใฝ%+๑๒๓๔ู฿๕๖๗๘๙๐"ฎฑธํ๊ณฯญฐ,ฅฤฆฏโฌ็๋ษศซ.ฦฬฒ?์ฺฮฉ)('.split(
|
|
958
|
+
"",
|
|
959
|
+
);
|
|
718
960
|
this.shuffleArray(ThaiAlphabet);
|
|
719
961
|
keys.forEach((key, index) => {
|
|
720
|
-
|
|
721
|
-
|
|
962
|
+
if (index < ThaiAlphabet.length) {
|
|
963
|
+
key.textContent = ThaiAlphabet[index];
|
|
964
|
+
key.dataset.key = ThaiAlphabet[index];
|
|
965
|
+
}
|
|
722
966
|
});
|
|
723
967
|
}
|
|
724
968
|
|
|
725
|
-
//
|
|
969
|
+
// FIX BUG-7: แก้ CSS selector ของ scrambleSymbols
|
|
726
970
|
scrambleSymbols() {
|
|
727
971
|
const keys = document.querySelectorAll(
|
|
728
|
-
":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'])",
|
|
729
973
|
);
|
|
730
|
-
const
|
|
731
|
-
this.shuffleArray(
|
|
974
|
+
const symbols = "@#$%^&*()_+~`{}|\\:'<>?/[]±§¶!€£¥¢©®™℅‰†".split("");
|
|
975
|
+
this.shuffleArray(symbols);
|
|
732
976
|
keys.forEach((key, index) => {
|
|
733
|
-
|
|
734
|
-
|
|
977
|
+
if (index < symbols.length) {
|
|
978
|
+
key.textContent = symbols[index];
|
|
979
|
+
key.dataset.key = symbols[index];
|
|
980
|
+
}
|
|
735
981
|
});
|
|
736
982
|
}
|
|
737
983
|
|
|
738
|
-
|
|
739
984
|
scrambleFullLayoutKeys() {
|
|
740
985
|
const keys = document.querySelectorAll(
|
|
741
|
-
".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 ↹'])",
|
|
742
987
|
);
|
|
743
|
-
const englishChars =
|
|
988
|
+
const englishChars =
|
|
989
|
+
"abcdefghijklmnopqrstuvwxyz1234567890;'\\/][`,.-=".split("");
|
|
744
990
|
this.shuffleArray(englishChars);
|
|
745
991
|
keys.forEach((key, index) => {
|
|
746
992
|
if (index < englishChars.length) {
|
|
@@ -750,20 +996,24 @@ export class VirtualKeyboard {
|
|
|
750
996
|
});
|
|
751
997
|
}
|
|
752
998
|
|
|
753
|
-
//
|
|
999
|
+
// FIX BUG-2: เก็บ bound reference และ cleanup ให้ถูกต้อง
|
|
754
1000
|
startDrag(event) {
|
|
755
1001
|
this.isDragging = true;
|
|
756
|
-
this.offsetX =
|
|
757
|
-
|
|
1002
|
+
this.offsetX =
|
|
1003
|
+
event.clientX - document.getElementById("keyboard").offsetLeft;
|
|
1004
|
+
this.offsetY =
|
|
1005
|
+
event.clientY - document.getElementById("keyboard").offsetTop;
|
|
758
1006
|
|
|
759
|
-
document.addEventListener("mousemove", this.
|
|
760
|
-
document.addEventListener("mouseup",
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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);
|
|
764
1015
|
}
|
|
765
1016
|
|
|
766
|
-
// [15]
|
|
767
1017
|
drag(event) {
|
|
768
1018
|
if (this.isDragging) {
|
|
769
1019
|
const keyboard = document.getElementById("keyboard");
|
|
@@ -771,4 +1021,4 @@ export class VirtualKeyboard {
|
|
|
771
1021
|
keyboard.style.top = `${event.clientY - this.offsetY}px`;
|
|
772
1022
|
}
|
|
773
1023
|
}
|
|
774
|
-
}
|
|
1024
|
+
}
|