js_lis 3.0.0 → 3.1.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/README.md +164 -0
- package/VirtualKeyboard.js +742 -787
- package/package.json +2 -2
- package/style/kbstyle.js +151 -5
package/VirtualKeyboard.js
CHANGED
|
@@ -1,6 +1,188 @@
|
|
|
1
1
|
import { layouts } from "./layouts.js";
|
|
2
|
-
import { injectCSS } from "./style/kbstyle.js";
|
|
2
|
+
import { getCSS, injectCSS } from "./style/kbstyle.js";
|
|
3
3
|
|
|
4
|
+
//AES-GCM ผ่าน WeakMap
|
|
5
|
+
class SecureBufferStore {
|
|
6
|
+
constructor() {
|
|
7
|
+
this._store = new WeakMap();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
//สร้างกุญแจเข้ารหัส
|
|
11
|
+
async _getEntry(inputEl) {
|
|
12
|
+
if (!this._store.has(inputEl)) {
|
|
13
|
+
const key = await crypto.subtle.generateKey(
|
|
14
|
+
{ name: "AES-GCM", length: 256 },
|
|
15
|
+
false, // ไม่ export ออก
|
|
16
|
+
["encrypt", "decrypt"]
|
|
17
|
+
);
|
|
18
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
19
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
20
|
+
{ name: "AES-GCM", iv },
|
|
21
|
+
key,
|
|
22
|
+
new TextEncoder().encode("")
|
|
23
|
+
);
|
|
24
|
+
this._store.set(inputEl, { key, iv, ciphertext });
|
|
25
|
+
}
|
|
26
|
+
return this._store.get(inputEl);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//ถอดรหัสชั่วคราว
|
|
30
|
+
async read(inputEl) {
|
|
31
|
+
if (!this._store.has(inputEl)) return "";
|
|
32
|
+
const { key, iv, ciphertext } = this._store.get(inputEl);
|
|
33
|
+
try {
|
|
34
|
+
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
|
|
35
|
+
return new TextDecoder().decode(plain);
|
|
36
|
+
} catch {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//เข้ารหัสใหม่
|
|
42
|
+
async write(inputEl, plaintext) {
|
|
43
|
+
const entry = await this._getEntry(inputEl);
|
|
44
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
45
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
46
|
+
{ name: "AES-GCM", iv },
|
|
47
|
+
entry.key,
|
|
48
|
+
new TextEncoder().encode(plaintext)
|
|
49
|
+
);
|
|
50
|
+
entry.iv = iv;
|
|
51
|
+
entry.ciphertext = ciphertext;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//ค่าเริ่มต้นให้กับ Input
|
|
55
|
+
async init(inputEl, initialPlaintext) {
|
|
56
|
+
if (!this._store.has(inputEl)) {
|
|
57
|
+
await this._getEntry(inputEl);
|
|
58
|
+
}
|
|
59
|
+
await this.write(inputEl, initialPlaintext);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
has(inputEl) { return this._store.has(inputEl); }
|
|
63
|
+
delete(inputEl) { this._store.delete(inputEl); }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//จัดการการพิมพ์
|
|
67
|
+
class IMEReconciliationEngine {
|
|
68
|
+
constructor() {
|
|
69
|
+
this._sessions = new Map();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//เริ่มต้น
|
|
73
|
+
start(inputEl, cursorStart, cursorEnd) {
|
|
74
|
+
this._sessions.set(inputEl, {
|
|
75
|
+
active: true,
|
|
76
|
+
compositionText: "",
|
|
77
|
+
anchorStart: cursorStart,
|
|
78
|
+
anchorEnd: cursorEnd,
|
|
79
|
+
lastDisplayLen: 0,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
isActive(inputEl) {
|
|
84
|
+
return this._sessions.get(inputEl)?.active ?? false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
//อัปเดตตอนพิมพ์
|
|
88
|
+
update(inputEl, newCompositionText) {
|
|
89
|
+
const s = this._sessions.get(inputEl);
|
|
90
|
+
if (!s) return null;
|
|
91
|
+
const prev = s.compositionText;
|
|
92
|
+
s.compositionText = newCompositionText;
|
|
93
|
+
s.lastDisplayLen = newCompositionText.length;
|
|
94
|
+
return {
|
|
95
|
+
replaceStart: s.anchorStart,
|
|
96
|
+
replaceEnd: s.anchorStart + prev.length,
|
|
97
|
+
insertText: newCompositionText,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//จบและส่งข้อความ
|
|
102
|
+
end(inputEl, finalText) {
|
|
103
|
+
const s = this._sessions.get(inputEl);
|
|
104
|
+
if (!s) return null;
|
|
105
|
+
const op = {
|
|
106
|
+
replaceStart: s.anchorStart,
|
|
107
|
+
replaceEnd: s.anchorStart + s.compositionText.length,
|
|
108
|
+
insertText: finalText,
|
|
109
|
+
};
|
|
110
|
+
this._sessions.delete(inputEl);
|
|
111
|
+
return op;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//ยกเลิกการพิมพ์
|
|
115
|
+
abort(inputEl) {
|
|
116
|
+
const s = this._sessions.get(inputEl);
|
|
117
|
+
if (!s) return null;
|
|
118
|
+
const op = {
|
|
119
|
+
replaceStart: s.anchorStart,
|
|
120
|
+
replaceEnd: s.anchorStart + s.compositionText.length,
|
|
121
|
+
insertText: "",
|
|
122
|
+
};
|
|
123
|
+
this._sessions.delete(inputEl);
|
|
124
|
+
return op;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
//ป้องกันดักอ่าน Clipboard
|
|
129
|
+
class AntiClipboardSniff {
|
|
130
|
+
constructor() {
|
|
131
|
+
this._copyListeners = [];
|
|
132
|
+
this._pasteListeners = [];
|
|
133
|
+
this._active = false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
//
|
|
137
|
+
install(getIsVKActive) {
|
|
138
|
+
if (this._active) return;
|
|
139
|
+
this._active = true;
|
|
140
|
+
this._getIsVKActive = getIsVKActive;
|
|
141
|
+
|
|
142
|
+
this._onCopy = (e) => {
|
|
143
|
+
if (!this._getIsVKActive()) return;
|
|
144
|
+
e.stopImmediatePropagation();
|
|
145
|
+
};
|
|
146
|
+
this._onCut = (e) => {
|
|
147
|
+
if (!this._getIsVKActive()) return;
|
|
148
|
+
e.stopImmediatePropagation();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
document.addEventListener("copy", this._onCopy, { capture: true });
|
|
152
|
+
document.addEventListener("cut", this._onCut, { capture: true });
|
|
153
|
+
|
|
154
|
+
//block การใช้คำสั่ง JS ของหน้าเว็บ
|
|
155
|
+
const _origExecCommand = document.execCommand.bind(document);
|
|
156
|
+
document.execCommand = (cmd, ...args) => {
|
|
157
|
+
if (
|
|
158
|
+
this._getIsVKActive() &&
|
|
159
|
+
(cmd === "copy" || cmd === "cut" || cmd === "insertText")
|
|
160
|
+
) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return _origExecCommand(cmd, ...args);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
uninstall() {
|
|
168
|
+
if (!this._active) return;
|
|
169
|
+
document.removeEventListener("copy", this._onCopy, { capture: true });
|
|
170
|
+
document.removeEventListener("cut", this._onCut, { capture: true });
|
|
171
|
+
this._active = false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//อ่านค่า Clipboard ผ่าน API
|
|
175
|
+
async securePaste() {
|
|
176
|
+
try {
|
|
177
|
+
if (navigator.clipboard?.readText) {
|
|
178
|
+
return await navigator.clipboard.readText();
|
|
179
|
+
}
|
|
180
|
+
} catch {}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
//VirtualKeyboard
|
|
4
186
|
export class VirtualKeyboard {
|
|
5
187
|
constructor() {
|
|
6
188
|
this.currentLayout = "full";
|
|
@@ -15,14 +197,19 @@ export class VirtualKeyboard {
|
|
|
15
197
|
this.capsLockActive = false;
|
|
16
198
|
this.isScrambled = false;
|
|
17
199
|
this.isVirtualKeyboardActive = false;
|
|
200
|
+
this.isInternalUpdate = false;
|
|
201
|
+
|
|
202
|
+
this.maskChar = "•";
|
|
203
|
+
|
|
204
|
+
this._secureStore = new SecureBufferStore();
|
|
205
|
+
this._imeEngine = new IMEReconciliationEngine();
|
|
206
|
+
this._antiClip = new AntiClipboardSniff();
|
|
18
207
|
|
|
19
|
-
this.
|
|
208
|
+
this._shadowRoot = null;
|
|
209
|
+
this._shadowHost = null;
|
|
20
210
|
|
|
21
|
-
// FIX BUG-2: เก็บ bound reference เพื่อให้ removeEventListener ทำงานได้
|
|
22
211
|
this._boundDrag = this.drag.bind(this);
|
|
23
212
|
this._boundStopDrag = this.stopDrag.bind(this);
|
|
24
|
-
|
|
25
|
-
// FIX WARN-3: เก็บ flag ว่า listeners ถูก add ไปแล้วหรือยัง
|
|
26
213
|
this._listenersInitialized = false;
|
|
27
214
|
|
|
28
215
|
this.initialize();
|
|
@@ -30,348 +217,428 @@ export class VirtualKeyboard {
|
|
|
30
217
|
|
|
31
218
|
async initialize() {
|
|
32
219
|
try {
|
|
33
|
-
|
|
220
|
+
this._antiClip.install(() => this.isVirtualKeyboardActive);
|
|
221
|
+
this._mountShadowDOM();
|
|
34
222
|
this.render();
|
|
35
|
-
// FIX WARN-3: เรียก initializeInputListeners ครั้งเดียวเท่านั้น
|
|
36
223
|
if (!this._listenersInitialized) {
|
|
37
224
|
this.initializeInputListeners();
|
|
38
225
|
this._listenersInitialized = true;
|
|
39
226
|
}
|
|
40
|
-
console.log("VirtualKeyboard initialized
|
|
41
|
-
} catch (
|
|
42
|
-
console.error("
|
|
227
|
+
console.log("VirtualKeyboard (secure) initialized.");
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.error("VirtualKeyboard init error:", err);
|
|
43
230
|
}
|
|
44
231
|
}
|
|
45
232
|
|
|
233
|
+
//ป้องกัน CSS/JS ภายนอกเข้าถึง
|
|
234
|
+
_mountShadowDOM() {
|
|
235
|
+
this._shadowHost = document.createElement("div");
|
|
236
|
+
this._shadowHost.id = "vk-shadow-host";
|
|
237
|
+
this.container.appendChild(this._shadowHost);
|
|
238
|
+
|
|
239
|
+
this._shadowRoot = this._shadowHost.attachShadow({ mode: "closed" });
|
|
240
|
+
|
|
241
|
+
const styleEl = document.createElement("style");
|
|
242
|
+
styleEl.textContent = this._getShadowStyles();
|
|
243
|
+
this._shadowRoot.appendChild(styleEl);
|
|
244
|
+
|
|
245
|
+
const inner = document.createElement("div");
|
|
246
|
+
inner.id = "keyboard-shadow-inner";
|
|
247
|
+
this._shadowRoot.appendChild(inner);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_getShadowStyles() {
|
|
251
|
+
return getCSS();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_q(selector) {
|
|
255
|
+
return this._shadowRoot?.querySelector(selector) ?? null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_qAll(selector) {
|
|
259
|
+
return this._shadowRoot?.querySelectorAll(selector) ?? [];
|
|
260
|
+
}
|
|
261
|
+
|
|
46
262
|
getLayoutName(layout) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
263
|
+
const names = {
|
|
264
|
+
full: "Full Keyboard", en: "English Keyboard", th: "Thai keyboard",
|
|
265
|
+
numpad: "Numpad Keyboard", symbols: "Symbols Keyboard",
|
|
266
|
+
"full-layouts": "Full English + Numpad",
|
|
267
|
+
};
|
|
268
|
+
return names[layout] ?? "Unknown Layout";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_isPasswordField(el) {
|
|
272
|
+
return el?.type === "password";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async getCurrentBuffer() {
|
|
276
|
+
if (!this.currentInput) return "";
|
|
277
|
+
return this._secureStore.read(this.currentInput);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async getBufferForInput(inputEl) {
|
|
281
|
+
if (!inputEl) return "";
|
|
282
|
+
return this._secureStore.read(inputEl);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async setCurrentBuffer(plaintext) {
|
|
286
|
+
if (!this.currentInput) return;
|
|
287
|
+
await this._secureStore.write(this.currentInput, plaintext);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
//อัปเดตค่าที่แสดงใน Input
|
|
291
|
+
async updateDisplayedValue(preserveCursor = true) {
|
|
292
|
+
if (!this.currentInput || this.isInternalUpdate) return;
|
|
293
|
+
|
|
294
|
+
const buffer = await this.getCurrentBuffer();
|
|
295
|
+
const isPassword = this._isPasswordField(this.currentInput);
|
|
296
|
+
|
|
297
|
+
const start = preserveCursor ? this.currentInput.selectionStart : null;
|
|
298
|
+
const end = preserveCursor ? this.currentInput.selectionEnd : null;
|
|
299
|
+
|
|
300
|
+
this.isInternalUpdate = true;
|
|
301
|
+
|
|
302
|
+
this.currentInput.value = isPassword
|
|
303
|
+
? this.maskChar.repeat(buffer.length)
|
|
304
|
+
: buffer;
|
|
305
|
+
|
|
306
|
+
if (preserveCursor && start !== null) {
|
|
307
|
+
this.currentInput.setSelectionRange(start, end);
|
|
62
308
|
}
|
|
309
|
+
|
|
310
|
+
this.isInternalUpdate = false;
|
|
63
311
|
}
|
|
64
312
|
|
|
313
|
+
//ตั้งค่า listeners สำหรับการโฟกัส, IME, clipboard
|
|
65
314
|
initializeInputListeners() {
|
|
66
315
|
document.addEventListener("click", (e) => {
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
const
|
|
70
|
-
this.setCurrentInput(
|
|
316
|
+
const t = e.target;
|
|
317
|
+
if (t.tagName === "INPUT" || t.tagName === "TEXTAREA") {
|
|
318
|
+
const pos = t.selectionStart;
|
|
319
|
+
this.setCurrentInput(t, !this.isVirtualKeyboardActive);
|
|
71
320
|
setTimeout(() => {
|
|
72
|
-
if (
|
|
73
|
-
target.setSelectionRange(clickCursorPos, clickCursorPos);
|
|
74
|
-
}
|
|
321
|
+
if (t.selectionStart !== pos) t.setSelectionRange(pos, pos);
|
|
75
322
|
}, 10);
|
|
76
323
|
}
|
|
77
324
|
});
|
|
78
325
|
|
|
79
|
-
document.addEventListener(
|
|
80
|
-
|
|
81
|
-
(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
true,
|
|
88
|
-
);
|
|
326
|
+
document.addEventListener("focus", (e) => {
|
|
327
|
+
const t = e.target;
|
|
328
|
+
if (t.tagName === "INPUT" || t.tagName === "TEXTAREA") {
|
|
329
|
+
this.setCurrentInput(t, !this.isVirtualKeyboardActive);
|
|
330
|
+
}
|
|
331
|
+
}, true);
|
|
89
332
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const target = e.target;
|
|
333
|
+
document.addEventListener("blur", (e) => {
|
|
334
|
+
const t = e.target;
|
|
93
335
|
if (
|
|
94
|
-
(
|
|
95
|
-
|
|
96
|
-
!this.isVirtualKeyboardActive
|
|
336
|
+
(t.tagName === "INPUT" || t.tagName === "TEXTAREA") &&
|
|
337
|
+
this._imeEngine.isActive(t)
|
|
97
338
|
) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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);
|
|
339
|
+
this._imeAbortAsync(t);
|
|
340
|
+
}
|
|
341
|
+
}, true);
|
|
117
342
|
|
|
118
|
-
|
|
119
|
-
|
|
343
|
+
//IME Reconciliation
|
|
344
|
+
document.addEventListener("compositionstart", (e) => {
|
|
345
|
+
const t = e.target;
|
|
346
|
+
if ((t.tagName === "INPUT" || t.tagName === "TEXTAREA") && t === this.currentInput) {
|
|
347
|
+
this._imeEngine.start(t, t.selectionStart, t.selectionEnd);
|
|
120
348
|
}
|
|
121
349
|
});
|
|
122
350
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const target = e.target;
|
|
351
|
+
document.addEventListener("compositionupdate", (e) => {
|
|
352
|
+
const t = e.target;
|
|
126
353
|
if (
|
|
127
|
-
(
|
|
128
|
-
|
|
129
|
-
|
|
354
|
+
(t.tagName === "INPUT" || t.tagName === "TEXTAREA") &&
|
|
355
|
+
t === this.currentInput &&
|
|
356
|
+
this._imeEngine.isActive(t)
|
|
130
357
|
) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
358
|
+
this._imeUpdateAsync(t, e.data ?? "");
|
|
359
|
+
}
|
|
360
|
+
});
|
|
134
361
|
|
|
135
|
-
|
|
136
|
-
|
|
362
|
+
document.addEventListener("compositionend", (e) => {
|
|
363
|
+
const t = e.target;
|
|
364
|
+
if (
|
|
365
|
+
(t.tagName === "INPUT" || t.tagName === "TEXTAREA") &&
|
|
366
|
+
t === this.currentInput &&
|
|
367
|
+
this._imeEngine.isActive(t)
|
|
368
|
+
) {
|
|
369
|
+
this._imeEndAsync(t, e.data ?? "");
|
|
370
|
+
}
|
|
371
|
+
});
|
|
137
372
|
|
|
138
|
-
|
|
139
|
-
|
|
373
|
+
//คีย์บอร์ดจริง
|
|
374
|
+
document.addEventListener("keydown", (e) => {
|
|
375
|
+
const t = e.target;
|
|
376
|
+
if ((t.tagName === "INPUT" || t.tagName === "TEXTAREA") && t === this.currentInput) {
|
|
377
|
+
if (this._imeEngine.isActive(t)) return;
|
|
140
378
|
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
this.updateDisplayedValue(false);
|
|
379
|
+
const navKeys = ["ArrowLeft","ArrowRight","ArrowUp","ArrowDown","Home","End"];
|
|
380
|
+
if (!navKeys.includes(e.code)) this.isVirtualKeyboardActive = false;
|
|
144
381
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}, 0);
|
|
382
|
+
if (!this.isVirtualKeyboardActive) {
|
|
383
|
+
this.handleRealKeyboardKeydown(t, e);
|
|
384
|
+
}
|
|
149
385
|
|
|
150
|
-
|
|
151
|
-
target.dispatchEvent(event);
|
|
386
|
+
if (navKeys.includes(e.code)) setTimeout(() => t.focus(), 0);
|
|
152
387
|
}
|
|
153
388
|
});
|
|
154
389
|
|
|
155
390
|
document.addEventListener("input", (e) => {
|
|
156
|
-
const
|
|
391
|
+
const t = e.target;
|
|
157
392
|
if (
|
|
158
|
-
(
|
|
159
|
-
|
|
393
|
+
(t.tagName === "INPUT" || t.tagName === "TEXTAREA") &&
|
|
394
|
+
t === this.currentInput &&
|
|
395
|
+
!this.isVirtualKeyboardActive &&
|
|
396
|
+
!this.isInternalUpdate
|
|
160
397
|
) {
|
|
161
|
-
|
|
162
|
-
this.handleRealKeyboardInput(target, e);
|
|
163
|
-
}
|
|
398
|
+
this._handleRealKeyboardInputAsync(t, e);
|
|
164
399
|
}
|
|
165
400
|
});
|
|
166
401
|
|
|
167
|
-
document.addEventListener("
|
|
168
|
-
const
|
|
402
|
+
document.addEventListener("paste", async (e) => {
|
|
403
|
+
const t = e.target;
|
|
169
404
|
if (
|
|
170
|
-
(
|
|
171
|
-
|
|
405
|
+
(t.tagName === "INPUT" || t.tagName === "TEXTAREA") &&
|
|
406
|
+
t === this.currentInput
|
|
172
407
|
) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
"ArrowRight",
|
|
176
|
-
"ArrowUp",
|
|
177
|
-
"ArrowDown",
|
|
178
|
-
"Home",
|
|
179
|
-
"End",
|
|
180
|
-
];
|
|
181
|
-
|
|
182
|
-
if (!navKeys.includes(e.code)) {
|
|
183
|
-
this.isVirtualKeyboardActive = false;
|
|
184
|
-
}
|
|
408
|
+
e.preventDefault();
|
|
409
|
+
e.stopImmediatePropagation();
|
|
185
410
|
|
|
186
|
-
|
|
187
|
-
|
|
411
|
+
let pasteText = await this._antiClip.securePaste();
|
|
412
|
+
if (pasteText === null) {
|
|
413
|
+
pasteText = (e.clipboardData || window.clipboardData)?.getData("text") ?? "";
|
|
188
414
|
}
|
|
415
|
+
if (!pasteText) return;
|
|
189
416
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
417
|
+
const start = t.selectionStart;
|
|
418
|
+
const end = t.selectionEnd;
|
|
419
|
+
const buffer = await this.getCurrentBuffer();
|
|
420
|
+
await this.setCurrentBuffer(buffer.slice(0, start) + pasteText + buffer.slice(end));
|
|
421
|
+
await this.updateDisplayedValue(false);
|
|
422
|
+
|
|
423
|
+
const newCursor = start + pasteText.length;
|
|
424
|
+
setTimeout(() => { t.setSelectionRange(newCursor, newCursor); t.focus(); }, 0);
|
|
425
|
+
|
|
426
|
+
this.isInternalUpdate = true;
|
|
427
|
+
t.dispatchEvent(new Event("input", { bubbles: true }));
|
|
428
|
+
this.isInternalUpdate = false;
|
|
195
429
|
}
|
|
196
|
-
});
|
|
430
|
+
}, { capture: true });
|
|
197
431
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
432
|
+
document.addEventListener("cut", async (e) => {
|
|
433
|
+
const t = e.target;
|
|
434
|
+
if (
|
|
435
|
+
(t.tagName === "INPUT" || t.tagName === "TEXTAREA") &&
|
|
436
|
+
t === this.currentInput
|
|
437
|
+
) {
|
|
438
|
+
const start = t.selectionStart;
|
|
439
|
+
const end = t.selectionEnd;
|
|
440
|
+
if (start === end) return;
|
|
441
|
+
|
|
442
|
+
e.preventDefault();
|
|
443
|
+
e.stopImmediatePropagation();
|
|
444
|
+
|
|
445
|
+
const buffer = await this.getCurrentBuffer();
|
|
446
|
+
const selected = buffer.slice(start, end);
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
if (navigator.clipboard?.writeText) {
|
|
450
|
+
await navigator.clipboard.writeText(selected);
|
|
451
|
+
} else {
|
|
452
|
+
e.clipboardData?.setData("text/plain", selected);
|
|
453
|
+
}
|
|
454
|
+
} catch {}
|
|
455
|
+
|
|
456
|
+
await this.setCurrentBuffer(buffer.slice(0, start) + buffer.slice(end));
|
|
457
|
+
await this.updateDisplayedValue(false);
|
|
458
|
+
|
|
459
|
+
setTimeout(() => { t.setSelectionRange(start, start); t.focus(); }, 0);
|
|
460
|
+
|
|
461
|
+
this.isInternalUpdate = true;
|
|
462
|
+
t.dispatchEvent(new Event("input", { bubbles: true }));
|
|
463
|
+
this.isInternalUpdate = false;
|
|
464
|
+
}
|
|
465
|
+
}, { capture: true });
|
|
466
|
+
|
|
467
|
+
document.getElementById("toggle")
|
|
468
|
+
?.addEventListener("click", this.toggle.bind(this));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
//จัดการ IME แบบ Async
|
|
472
|
+
async _imeUpdateAsync(inputEl, compositionText) {
|
|
473
|
+
const op = this._imeEngine.update(inputEl, compositionText);
|
|
474
|
+
if (!op) return;
|
|
475
|
+
|
|
476
|
+
const buffer = await this.getBufferForInput(inputEl);
|
|
477
|
+
const newBuf = buffer.slice(0, op.replaceStart) + op.insertText + buffer.slice(op.replaceEnd);
|
|
478
|
+
await this._secureStore.write(inputEl, newBuf);
|
|
479
|
+
|
|
480
|
+
const newCursor = op.replaceStart + op.insertText.length;
|
|
481
|
+
this.isInternalUpdate = true;
|
|
482
|
+
inputEl.value = this._isPasswordField(inputEl)
|
|
483
|
+
? this.maskChar.repeat(newBuf.length)
|
|
484
|
+
: newBuf;
|
|
485
|
+
inputEl.setSelectionRange(newCursor, newCursor);
|
|
486
|
+
this.isInternalUpdate = false;
|
|
202
487
|
}
|
|
203
488
|
|
|
489
|
+
async _imeEndAsync(inputEl, finalText) {
|
|
490
|
+
const op = this._imeEngine.end(inputEl, finalText);
|
|
491
|
+
if (!op) return;
|
|
492
|
+
|
|
493
|
+
const buffer = await this.getBufferForInput(inputEl);
|
|
494
|
+
const newBuf = buffer.slice(0, op.replaceStart) + op.insertText + buffer.slice(op.replaceEnd);
|
|
495
|
+
await this._secureStore.write(inputEl, newBuf);
|
|
496
|
+
|
|
497
|
+
const newCursor = op.replaceStart + op.insertText.length;
|
|
498
|
+
this.isInternalUpdate = true;
|
|
499
|
+
inputEl.value = this._isPasswordField(inputEl)
|
|
500
|
+
? this.maskChar.repeat(newBuf.length)
|
|
501
|
+
: newBuf;
|
|
502
|
+
inputEl.setSelectionRange(newCursor, newCursor);
|
|
503
|
+
this.isInternalUpdate = false;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async _imeAbortAsync(inputEl) {
|
|
507
|
+
const op = this._imeEngine.abort(inputEl);
|
|
508
|
+
if (!op) return;
|
|
509
|
+
|
|
510
|
+
const buffer = await this.getBufferForInput(inputEl);
|
|
511
|
+
const newBuf = buffer.slice(0, op.replaceStart) + buffer.slice(op.replaceEnd);
|
|
512
|
+
await this._secureStore.write(inputEl, newBuf);
|
|
513
|
+
|
|
514
|
+
this.isInternalUpdate = true;
|
|
515
|
+
inputEl.value = this._isPasswordField(inputEl)
|
|
516
|
+
? this.maskChar.repeat(newBuf.length)
|
|
517
|
+
: newBuf;
|
|
518
|
+
inputEl.setSelectionRange(op.replaceStart, op.replaceStart);
|
|
519
|
+
this.isInternalUpdate = false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
//setCurrentInput
|
|
204
523
|
setCurrentInput(inputElement, moveToEnd = true) {
|
|
205
|
-
if (this.currentInput)
|
|
206
|
-
this.currentInput.classList.remove("keyboard-active");
|
|
207
|
-
}
|
|
524
|
+
if (this.currentInput) this.currentInput.classList.remove("keyboard-active");
|
|
208
525
|
|
|
209
526
|
this.currentInput = inputElement;
|
|
210
527
|
this.currentInput.classList.add("keyboard-active");
|
|
211
528
|
this.currentInput.focus();
|
|
212
529
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.currentInput.
|
|
223
|
-
|
|
224
|
-
|
|
530
|
+
const initBuffer = async () => {
|
|
531
|
+
if (!this._secureStore.has(this.currentInput)) {
|
|
532
|
+
const initial = this._isPasswordField(this.currentInput)
|
|
533
|
+
? ""
|
|
534
|
+
: (this.currentInput.value || "");
|
|
535
|
+
await this._secureStore.init(this.currentInput, initial);
|
|
536
|
+
}
|
|
537
|
+
await this.updateDisplayedValue(false);
|
|
538
|
+
if (moveToEnd) {
|
|
539
|
+
const len = this.currentInput.value.length;
|
|
540
|
+
this.currentInput.setSelectionRange(len, len);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
initBuffer();
|
|
225
544
|
}
|
|
226
545
|
|
|
546
|
+
//render shadow DOM
|
|
227
547
|
render() {
|
|
548
|
+
const inner = this._shadowRoot?.getElementById("keyboard-shadow-inner");
|
|
549
|
+
if (!inner) return;
|
|
550
|
+
|
|
228
551
|
const keyboard = document.createElement("div");
|
|
229
552
|
keyboard.className = `virtual-keyboard ${this.currentLayout}`;
|
|
230
553
|
keyboard.style.display = this.isVisible ? "block" : "none";
|
|
231
554
|
keyboard.id = "keyboard";
|
|
232
555
|
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
controlsContainer.style.marginBottom = "10px";
|
|
239
|
-
|
|
240
|
-
const layoutSelector = document.createElement("select");
|
|
241
|
-
layoutSelector.id = "layout-selector";
|
|
242
|
-
layoutSelector.setAttribute("aria-label", "Select keyboard layout");
|
|
243
|
-
layoutSelector.onchange = (e) => this.changeLayout(e.target.value);
|
|
244
|
-
|
|
245
|
-
const availableLayouts = ["full", "en", "th", "numpad", "symbols"];
|
|
246
|
-
availableLayouts.forEach((layout) => {
|
|
247
|
-
const option = document.createElement("option");
|
|
248
|
-
option.value = layout;
|
|
249
|
-
option.innerText = this.getLayoutName(layout);
|
|
250
|
-
layoutSelector.appendChild(option);
|
|
556
|
+
const controls = document.createElement("div");
|
|
557
|
+
controls.className = "controls";
|
|
558
|
+
Object.assign(controls.style, {
|
|
559
|
+
display: "flex", justifyContent: "center",
|
|
560
|
+
alignItems: "center", marginBottom: "10px",
|
|
251
561
|
});
|
|
252
|
-
layoutSelector.value = this.currentLayout;
|
|
253
|
-
controlsContainer.appendChild(layoutSelector);
|
|
254
|
-
|
|
255
|
-
keyboard.appendChild(controlsContainer);
|
|
256
|
-
|
|
257
|
-
const layout = this.layouts[this.currentLayout];
|
|
258
|
-
|
|
259
|
-
layout.forEach((row) => {
|
|
260
|
-
const rowElement = document.createElement("div");
|
|
261
|
-
rowElement.className = "keyboard-row";
|
|
262
|
-
|
|
263
|
-
row.forEach((key, index) => {
|
|
264
|
-
const keyElement = document.createElement("button");
|
|
265
|
-
keyElement.className = "keyboard-key key";
|
|
266
|
-
keyElement.textContent = key;
|
|
267
|
-
keyElement.type = "button";
|
|
268
|
-
keyElement.dataset.key = key;
|
|
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
|
-
|
|
280
|
-
if (index >= row.length - 4) {
|
|
281
|
-
keyElement.classList.add("concat-keys");
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (key === " ") {
|
|
285
|
-
keyElement.className += " spacebar";
|
|
286
|
-
keyElement.innerHTML = "";
|
|
287
|
-
}
|
|
288
562
|
|
|
563
|
+
const sel = document.createElement("select");
|
|
564
|
+
sel.id = "layout-selector";
|
|
565
|
+
sel.setAttribute("aria-label", "Select keyboard layout");
|
|
566
|
+
sel.onchange = (e) => this.changeLayout(e.target.value);
|
|
567
|
+
["full","en","th","numpad","symbols"].forEach((l) => {
|
|
568
|
+
const o = document.createElement("option");
|
|
569
|
+
o.value = l; o.innerText = this.getLayoutName(l);
|
|
570
|
+
sel.appendChild(o);
|
|
571
|
+
});
|
|
572
|
+
sel.value = this.currentLayout;
|
|
573
|
+
controls.appendChild(sel);
|
|
574
|
+
keyboard.appendChild(controls);
|
|
575
|
+
|
|
576
|
+
// Keys
|
|
577
|
+
this.layouts[this.currentLayout].forEach((row) => {
|
|
578
|
+
const rowEl = document.createElement("div");
|
|
579
|
+
rowEl.className = "keyboard-row";
|
|
580
|
+
row.forEach((key, i) => {
|
|
581
|
+
const btn = document.createElement("button");
|
|
582
|
+
btn.className = "keyboard-key key";
|
|
583
|
+
btn.textContent = key;
|
|
584
|
+
btn.type = "button";
|
|
585
|
+
btn.dataset.key = key;
|
|
586
|
+
btn.setAttribute("aria-label",
|
|
587
|
+
key === " " ? "spacebar" :
|
|
588
|
+
(key === "backspace" || key === "Backspace") ? "backspace" : key
|
|
589
|
+
);
|
|
590
|
+
if (i >= row.length - 4) btn.classList.add("concat-keys");
|
|
591
|
+
if (key === " ") { btn.className += " spacebar"; btn.innerHTML = ""; }
|
|
289
592
|
if (key === "backspace" || key === "Backspace") {
|
|
290
|
-
|
|
291
|
-
|
|
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");
|
|
593
|
+
btn.className += " backspacew";
|
|
594
|
+
btn.innerHTML = '⌫';
|
|
300
595
|
}
|
|
596
|
+
if (key === "Caps 🄰" && this.capsLockActive) btn.classList.add("active","bg-gray-400");
|
|
597
|
+
if (key === "Shift ⇧" && this.shiftActive) btn.classList.add("active","bg-gray-400");
|
|
301
598
|
|
|
302
|
-
|
|
599
|
+
btn.onclick = (e) => {
|
|
303
600
|
e.preventDefault();
|
|
304
|
-
const
|
|
305
|
-
if (
|
|
306
|
-
this.handleKeyPress(keyPressed);
|
|
307
|
-
} else {
|
|
308
|
-
console.error("The key element does not have a valid key value.");
|
|
309
|
-
}
|
|
601
|
+
const k = btn.dataset.key || btn.textContent;
|
|
602
|
+
if (k) this.handleKeyPress(k);
|
|
310
603
|
};
|
|
311
|
-
|
|
312
|
-
rowElement.appendChild(keyElement);
|
|
604
|
+
rowEl.appendChild(btn);
|
|
313
605
|
});
|
|
314
|
-
|
|
315
|
-
keyboard.appendChild(rowElement);
|
|
606
|
+
keyboard.appendChild(rowEl);
|
|
316
607
|
});
|
|
317
608
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
// FIX BUG-2: ใช้ bound reference แทน bind() inline
|
|
322
|
-
keyboard.addEventListener("mousedown", (event) => this.startDrag(event));
|
|
609
|
+
inner.innerHTML = "";
|
|
610
|
+
inner.appendChild(keyboard);
|
|
611
|
+
keyboard.addEventListener("mousedown", (ev) => this.startDrag(ev));
|
|
323
612
|
}
|
|
324
613
|
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
614
|
+
//handleKeyPress
|
|
615
|
+
|
|
616
|
+
async handleKeyPress(keyPressed) {
|
|
617
|
+
if (!this.currentInput || !keyPressed) return;
|
|
618
|
+
|
|
619
|
+
if (this._imeEngine.isActive(this.currentInput)) return;
|
|
328
620
|
|
|
329
621
|
this.isVirtualKeyboardActive = true;
|
|
330
622
|
|
|
331
623
|
const start = this.currentInput.selectionStart;
|
|
332
624
|
const end = this.currentInput.selectionEnd;
|
|
333
|
-
const buffer = this.getCurrentBuffer();
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
const isShiftActive = this.shiftActive;
|
|
337
|
-
|
|
338
|
-
const convertToCorrectCase = (char) => {
|
|
339
|
-
if (isCapsActive || isShiftActive) return char.toUpperCase();
|
|
340
|
-
return char.toLowerCase();
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
if (!keyPressed) {
|
|
344
|
-
this.isVirtualKeyboardActive = false;
|
|
345
|
-
return console.error("Invalid key pressed.");
|
|
346
|
-
}
|
|
625
|
+
const buffer = await this.getCurrentBuffer();
|
|
626
|
+
const isCaps = this.capsLockActive;
|
|
627
|
+
const isShift = this.shiftActive;
|
|
347
628
|
|
|
629
|
+
//scramble
|
|
348
630
|
if (keyPressed === "scr" || keyPressed === "scrambled") {
|
|
349
|
-
const
|
|
350
|
-
en: () =>
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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(),
|
|
631
|
+
const h = {
|
|
632
|
+
en: () => this.isScrambled ? this.unscrambleKeys() : this.scrambleEnglishKeys(),
|
|
633
|
+
th: () => this.isScrambled ? this.unscrambleKeys() : this.scrambleThaiKeys(),
|
|
634
|
+
numpad: () => this.isScrambled ? this.unscrambleKeys() : this.scrambleKeyboard(),
|
|
635
|
+
symbols: () => this.isScrambled ? this.unscrambleKeys() : this.scrambleSymbols(),
|
|
636
|
+
full: () => this.isScrambled ? this.unscrambleKeys() : this.scrambleFullLayoutKeys(),
|
|
362
637
|
};
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
this.isScrambled = !this.isScrambled;
|
|
368
|
-
document
|
|
369
|
-
.querySelectorAll('.key[data-key="scr"], .key[data-key="scrambled"]')
|
|
370
|
-
.forEach((key) => {
|
|
371
|
-
key.classList.toggle("active", this.isScrambled);
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
|
|
638
|
+
h[this.currentLayout]?.();
|
|
639
|
+
this.isScrambled = !this.isScrambled;
|
|
640
|
+
this._qAll('.key[data-key="scr"],.key[data-key="scrambled"]')
|
|
641
|
+
.forEach((k) => k.classList.toggle("active", this.isScrambled));
|
|
375
642
|
this.isVirtualKeyboardActive = false;
|
|
376
643
|
return;
|
|
377
644
|
}
|
|
@@ -380,645 +647,333 @@ export class VirtualKeyboard {
|
|
|
380
647
|
case "HOME":
|
|
381
648
|
this.currentInput.setSelectionRange(0, 0);
|
|
382
649
|
this.currentInput.focus();
|
|
383
|
-
setTimeout(() => {
|
|
384
|
-
|
|
385
|
-
this.currentInput.focus();
|
|
386
|
-
}, 5);
|
|
387
|
-
this.isVirtualKeyboardActive = false;
|
|
388
|
-
return;
|
|
650
|
+
setTimeout(() => { this.currentInput.setSelectionRange(0, 0); this.currentInput.focus(); }, 5);
|
|
651
|
+
this.isVirtualKeyboardActive = false; return;
|
|
389
652
|
|
|
390
653
|
case "END": {
|
|
391
|
-
const
|
|
392
|
-
this.currentInput.setSelectionRange(
|
|
654
|
+
const len = buffer.length;
|
|
655
|
+
this.currentInput.setSelectionRange(len, len);
|
|
393
656
|
this.currentInput.focus();
|
|
394
|
-
setTimeout(() => {
|
|
395
|
-
|
|
396
|
-
this.currentInput.focus();
|
|
397
|
-
}, 5);
|
|
398
|
-
this.isVirtualKeyboardActive = false;
|
|
399
|
-
return;
|
|
657
|
+
setTimeout(() => { this.currentInput.setSelectionRange(len, len); this.currentInput.focus(); }, 5);
|
|
658
|
+
this.isVirtualKeyboardActive = false; return;
|
|
400
659
|
}
|
|
401
660
|
|
|
402
|
-
case "Backspace":
|
|
403
|
-
|
|
404
|
-
if (start === end && start > 0) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
410
|
-
} else if (start !== end) {
|
|
411
|
-
const newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
412
|
-
this.setCurrentBuffer(newBuffer);
|
|
413
|
-
this.updateDisplayedValue(false);
|
|
414
|
-
this.currentInput.setSelectionRange(start, start);
|
|
415
|
-
}
|
|
416
|
-
break;
|
|
417
|
-
|
|
418
|
-
case "DEL⌦":
|
|
419
|
-
if (start === end && start < buffer.length) {
|
|
420
|
-
const newBuffer = buffer.slice(0, start) + buffer.slice(end + 1);
|
|
421
|
-
this.setCurrentBuffer(newBuffer);
|
|
422
|
-
this.updateDisplayedValue(false);
|
|
423
|
-
this.currentInput.setSelectionRange(start, start);
|
|
424
|
-
} else if (start !== end) {
|
|
425
|
-
const newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
426
|
-
this.setCurrentBuffer(newBuffer);
|
|
427
|
-
this.updateDisplayedValue(false);
|
|
428
|
-
this.currentInput.setSelectionRange(start, start);
|
|
429
|
-
}
|
|
661
|
+
case "Backspace": case "backspace": {
|
|
662
|
+
let nb = buffer, np = start;
|
|
663
|
+
if (start === end && start > 0) { nb = buffer.slice(0, start - 1) + buffer.slice(end); np = start - 1; }
|
|
664
|
+
else if (start !== end) { nb = buffer.slice(0, start) + buffer.slice(end); np = start; }
|
|
665
|
+
await this.setCurrentBuffer(nb);
|
|
666
|
+
await this.updateDisplayedValue(false);
|
|
667
|
+
this.currentInput.setSelectionRange(np, np);
|
|
430
668
|
break;
|
|
669
|
+
}
|
|
431
670
|
|
|
432
|
-
case "
|
|
433
|
-
|
|
671
|
+
case "DEL⌦": {
|
|
672
|
+
let nb = buffer;
|
|
673
|
+
if (start === end && start < buffer.length) nb = buffer.slice(0, start) + buffer.slice(end + 1);
|
|
674
|
+
else if (start !== end) nb = buffer.slice(0, start) + buffer.slice(end);
|
|
675
|
+
await this.setCurrentBuffer(nb);
|
|
676
|
+
await this.updateDisplayedValue(false);
|
|
677
|
+
this.currentInput.setSelectionRange(start, start);
|
|
434
678
|
break;
|
|
679
|
+
}
|
|
435
680
|
|
|
436
|
-
case "
|
|
437
|
-
|
|
438
|
-
break;
|
|
681
|
+
case "Space": await this._insertText(" "); break;
|
|
682
|
+
case "Tab ↹": await this._insertText("\t"); break;
|
|
439
683
|
|
|
440
684
|
case "Enter":
|
|
441
685
|
if (this.currentInput.tagName === "TEXTAREA") {
|
|
442
|
-
const
|
|
443
|
-
this.setCurrentBuffer(
|
|
444
|
-
|
|
445
|
-
this.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
this.currentInput.
|
|
451
|
-
) {
|
|
452
|
-
if (this.currentInput.form) {
|
|
453
|
-
const submitButton = this.currentInput.form.querySelector(
|
|
454
|
-
'input[type="submit"], button[type="submit"], button[type="button"], button[onclick]',
|
|
455
|
-
);
|
|
456
|
-
if (submitButton) submitButton.click();
|
|
457
|
-
else this.currentInput.form.submit();
|
|
458
|
-
} else {
|
|
459
|
-
const newBuffer = buffer + "\n";
|
|
460
|
-
this.setCurrentBuffer(newBuffer);
|
|
461
|
-
this.updateDisplayedValue(false);
|
|
462
|
-
}
|
|
686
|
+
const nb = buffer.slice(0, start) + "\n" + buffer.slice(end);
|
|
687
|
+
await this.setCurrentBuffer(nb);
|
|
688
|
+
await this.updateDisplayedValue(false);
|
|
689
|
+
this.currentInput.setSelectionRange(start + 1, start + 1);
|
|
690
|
+
} else if (this.currentInput.form) {
|
|
691
|
+
const submit = this.currentInput.form.querySelector(
|
|
692
|
+
'input[type="submit"],button[type="submit"],button[type="button"],button[onclick]'
|
|
693
|
+
);
|
|
694
|
+
if (submit) submit.click(); else this.currentInput.form.submit();
|
|
463
695
|
}
|
|
464
696
|
break;
|
|
465
697
|
|
|
466
|
-
case "Caps 🄰":
|
|
467
|
-
|
|
468
|
-
break;
|
|
469
|
-
|
|
470
|
-
case "Shift ⇧":
|
|
471
|
-
this.toggleShift();
|
|
472
|
-
break;
|
|
698
|
+
case "Caps 🄰": this.toggleCapsLock(); break;
|
|
699
|
+
case "Shift ⇧": this.toggleShift(); break;
|
|
473
700
|
|
|
474
701
|
case "←": {
|
|
475
|
-
const
|
|
476
|
-
this.currentInput.setSelectionRange(
|
|
477
|
-
this.isVirtualKeyboardActive = false;
|
|
478
|
-
return;
|
|
702
|
+
const p = Math.max(0, start - 1);
|
|
703
|
+
this.currentInput.setSelectionRange(p, p);
|
|
704
|
+
this.isVirtualKeyboardActive = false; return;
|
|
479
705
|
}
|
|
480
|
-
|
|
481
706
|
case "→": {
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
this.
|
|
485
|
-
this.isVirtualKeyboardActive = false;
|
|
486
|
-
return;
|
|
707
|
+
const p = Math.min(buffer.length, start + 1);
|
|
708
|
+
this.currentInput.setSelectionRange(p, p);
|
|
709
|
+
this.isVirtualKeyboardActive = false; return;
|
|
487
710
|
}
|
|
488
711
|
|
|
489
|
-
case "↑":
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
start -
|
|
497
|
-
|
|
498
|
-
if (keyPressed === "↑" && actualCurrentLineIndex > 0) {
|
|
499
|
-
const prevLineLength = actualLines[actualCurrentLineIndex - 1].length;
|
|
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
|
-
);
|
|
712
|
+
case "↑": case "↓": {
|
|
713
|
+
const text = buffer;
|
|
714
|
+
const lines = text.substring(0, start).split("\n");
|
|
715
|
+
const li = lines.length - 1;
|
|
716
|
+
const col = start - text.lastIndexOf("\n", start - 1) - 1;
|
|
717
|
+
if (keyPressed === "↑" && li > 0) {
|
|
718
|
+
const prevLen = lines[li - 1].length;
|
|
719
|
+
const np = start - lines[li].length - 1 - Math.max(0, col - prevLen);
|
|
720
|
+
this.currentInput.setSelectionRange(Math.max(0, np), Math.max(0, np));
|
|
509
721
|
} else if (keyPressed === "↓") {
|
|
510
|
-
const
|
|
511
|
-
const
|
|
512
|
-
if (
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
nextLineEnd === -1 ? textAfterNextLineBreak.length : nextLineEnd;
|
|
519
|
-
const downNewPos =
|
|
520
|
-
start +
|
|
521
|
-
nextLineBreak +
|
|
522
|
-
1 +
|
|
523
|
-
Math.min(actualColumnIndex, nextLineLength);
|
|
524
|
-
this.currentInput.setSelectionRange(downNewPos, downNewPos);
|
|
722
|
+
const rem = text.substring(start);
|
|
723
|
+
const nl = rem.indexOf("\n");
|
|
724
|
+
if (nl !== -1) {
|
|
725
|
+
const afterNL = rem.substring(nl + 1);
|
|
726
|
+
const nextEnd = afterNL.indexOf("\n");
|
|
727
|
+
const nextLen = nextEnd === -1 ? afterNL.length : nextEnd;
|
|
728
|
+
const np = start + nl + 1 + Math.min(col, nextLen);
|
|
729
|
+
this.currentInput.setSelectionRange(np, np);
|
|
525
730
|
}
|
|
526
731
|
}
|
|
527
|
-
|
|
528
732
|
this.currentInput.focus();
|
|
529
|
-
this.isVirtualKeyboardActive = false;
|
|
530
|
-
return;
|
|
733
|
+
this.isVirtualKeyboardActive = false; return;
|
|
531
734
|
}
|
|
532
735
|
|
|
533
736
|
default: {
|
|
534
|
-
const
|
|
535
|
-
this.
|
|
737
|
+
const ch = (isCaps || isShift) ? keyPressed.toUpperCase() : keyPressed.toLowerCase();
|
|
738
|
+
await this._insertText(ch);
|
|
536
739
|
break;
|
|
537
740
|
}
|
|
538
741
|
}
|
|
539
742
|
|
|
540
|
-
if (
|
|
743
|
+
if (isShift && !isCaps) this.toggleShift();
|
|
541
744
|
|
|
542
745
|
this.currentInput.focus();
|
|
543
|
-
|
|
544
|
-
// FIX BUG-3: reset flag หลัง setTimeout เพื่อป้องกัน real keyboard listener เข้ามาแทรก
|
|
545
746
|
setTimeout(() => {
|
|
747
|
+
this.isVirtualKeyboardActive = false;
|
|
546
748
|
if (this.currentInput) {
|
|
749
|
+
const p = this.currentInput.selectionStart;
|
|
750
|
+
this.currentInput.setSelectionRange(p, p);
|
|
547
751
|
this.currentInput.focus();
|
|
548
|
-
const cursorPos = this.currentInput.selectionStart;
|
|
549
|
-
this.currentInput.setSelectionRange(cursorPos, cursorPos);
|
|
550
752
|
}
|
|
551
|
-
this.isVirtualKeyboardActive = false;
|
|
552
753
|
}, 0);
|
|
553
754
|
|
|
554
|
-
this.
|
|
555
|
-
|
|
556
|
-
this.
|
|
755
|
+
this.isInternalUpdate = true;
|
|
756
|
+
this.currentInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
757
|
+
this.isInternalUpdate = false;
|
|
557
758
|
}
|
|
558
759
|
|
|
559
|
-
|
|
760
|
+
//แทรกข้อความลงในตำแหน่ง Cursor และเข้ารหัสเก็บใน Store
|
|
761
|
+
async _insertText(text) {
|
|
762
|
+
if (!this.currentInput) return;
|
|
560
763
|
const start = this.currentInput.selectionStart;
|
|
561
764
|
const end = this.currentInput.selectionEnd;
|
|
765
|
+
const buffer = await this.getCurrentBuffer();
|
|
766
|
+
const newBuf = buffer.slice(0, start) + text + buffer.slice(end);
|
|
767
|
+
await this.setCurrentBuffer(newBuf);
|
|
562
768
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
this.
|
|
566
|
-
|
|
567
|
-
const newCursorPos = start + text.length;
|
|
568
|
-
this.updateDisplayedValue(false);
|
|
569
|
-
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
getCurrentBuffer() {
|
|
573
|
-
if (!this.currentInput) return "";
|
|
574
|
-
return this.inputPlaintextBuffers.get(this.currentInput) || "";
|
|
575
|
-
}
|
|
576
|
-
|
|
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
|
-
}
|
|
769
|
+
this.isInternalUpdate = true;
|
|
770
|
+
await this.updateDisplayedValue(false);
|
|
771
|
+
this.isInternalUpdate = false;
|
|
585
772
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
this.inputPlaintextBuffers.set(this.currentInput, newBuffer);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
updateDisplayedValue(preserveCursor = true) {
|
|
592
|
-
if (!this.currentInput) return;
|
|
593
|
-
const buffer = this.getCurrentBuffer();
|
|
594
|
-
const isPassword = this.currentInput.type === "password";
|
|
595
|
-
const maskChar = "•";
|
|
596
|
-
|
|
597
|
-
const currentStart = preserveCursor ? this.currentInput.selectionStart : 0;
|
|
598
|
-
const currentEnd = preserveCursor ? this.currentInput.selectionEnd : 0;
|
|
599
|
-
|
|
600
|
-
this.currentInput.value = isPassword
|
|
601
|
-
? maskChar.repeat(buffer.length)
|
|
602
|
-
: buffer;
|
|
603
|
-
|
|
604
|
-
if (preserveCursor) {
|
|
605
|
-
this.currentInput.setSelectionRange(currentStart, currentEnd);
|
|
606
|
-
}
|
|
773
|
+
const np = start + text.length;
|
|
774
|
+
this.currentInput.setSelectionRange(np, np);
|
|
607
775
|
}
|
|
608
776
|
|
|
609
|
-
|
|
777
|
+
//บันทึกลง Store (ป้องกันการเก็บ plaintext ใน .value)
|
|
778
|
+
async handleRealKeyboardKeydown(input, event) {
|
|
610
779
|
if (!input) return;
|
|
611
|
-
|
|
612
780
|
const key = event.key;
|
|
613
781
|
const start = input.selectionStart;
|
|
614
782
|
const end = input.selectionEnd;
|
|
615
|
-
const buffer = this.
|
|
783
|
+
const buffer = await this.getBufferForInput(input);
|
|
616
784
|
|
|
617
785
|
if (event.ctrlKey || event.altKey || event.metaKey) {
|
|
618
|
-
|
|
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
|
|
786
|
+
if ((event.ctrlKey || event.metaKey) && key === "a") return;
|
|
624
787
|
return;
|
|
625
788
|
}
|
|
626
789
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
}
|
|
790
|
+
const ignored = [
|
|
791
|
+
"Shift","Control","Alt","Meta","CapsLock","Tab","Escape",
|
|
792
|
+
"ArrowLeft","ArrowRight","ArrowUp","ArrowDown","Home","End",
|
|
793
|
+
"F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","F11","F12",
|
|
794
|
+
];
|
|
795
|
+
if (ignored.includes(key)) return;
|
|
658
796
|
|
|
659
|
-
let
|
|
797
|
+
let newBuf = buffer, newCursor = start;
|
|
660
798
|
|
|
661
799
|
if (key === "Backspace") {
|
|
662
|
-
if (start === end && start > 0) {
|
|
663
|
-
|
|
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);
|
|
800
|
+
if (start === end && start > 0) { newBuf = buffer.slice(0, start - 1) + buffer.slice(start); newCursor = start - 1; }
|
|
801
|
+
else if (start !== end) { newBuf = buffer.slice(0, start) + buffer.slice(end); }
|
|
677
802
|
} else if (key === "Delete") {
|
|
678
|
-
if (start === end && start < buffer.length)
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
803
|
+
if (start === end && start < buffer.length) newBuf = buffer.slice(0, start) + buffer.slice(start + 1);
|
|
804
|
+
else if (start !== end) newBuf = buffer.slice(0, start) + buffer.slice(end);
|
|
805
|
+
newCursor = start;
|
|
806
|
+
} else if (key === "Enter" && input.tagName === "TEXTAREA") {
|
|
807
|
+
newBuf = buffer.slice(0, start) + "\n" + buffer.slice(end);
|
|
808
|
+
newCursor = start + 1;
|
|
809
|
+
} else if (key.length === 1) {
|
|
810
|
+
newBuf = buffer.slice(0, start) + key + buffer.slice(end);
|
|
811
|
+
newCursor = start + 1;
|
|
812
|
+
} else { return; }
|
|
683
813
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
this.updateDisplayedValue(false);
|
|
814
|
+
event.preventDefault();
|
|
815
|
+
await this._secureStore.write(input, newBuf);
|
|
687
816
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if (input.tagName === "TEXTAREA") {
|
|
694
|
-
newBuffer = buffer.slice(0, start) + "\n" + buffer.slice(end);
|
|
817
|
+
this.isInternalUpdate = true;
|
|
818
|
+
input.value = this._isPasswordField(input)
|
|
819
|
+
? this.maskChar.repeat(newBuf.length)
|
|
820
|
+
: newBuf;
|
|
821
|
+
this.isInternalUpdate = false;
|
|
695
822
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
this.updateDisplayedValue(false);
|
|
823
|
+
setTimeout(() => { input.setSelectionRange(newCursor, newCursor); input.focus(); }, 0);
|
|
824
|
+
}
|
|
699
825
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
826
|
+
async _handleRealKeyboardInputAsync(input, event) {
|
|
827
|
+
if (!input || !event || this.isInternalUpdate) return;
|
|
828
|
+
// Autocomplete / browser autofill (not going through keydown)
|
|
829
|
+
if (event.inputType?.startsWith("insertReplacementText")) {
|
|
830
|
+
if (!this._isPasswordField(input)) {
|
|
831
|
+
await this._secureStore.write(input, input.value);
|
|
705
832
|
}
|
|
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
833
|
}
|
|
719
834
|
}
|
|
720
835
|
|
|
721
|
-
//
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
}
|
|
836
|
+
//scramble
|
|
837
|
+
|
|
838
|
+
unscrambleKeys() { this.render(); }
|
|
734
839
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
) {
|
|
740
|
-
this.inputPlaintextBuffers.set(input, input.value);
|
|
840
|
+
shuffleArray(arr) {
|
|
841
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
842
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
843
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
741
844
|
}
|
|
742
845
|
}
|
|
743
846
|
|
|
744
|
-
|
|
745
|
-
this.
|
|
847
|
+
_scramble(selector, chars) {
|
|
848
|
+
this._qAll(selector).forEach((k, i) => {
|
|
849
|
+
if (i < chars.length) { k.textContent = chars[i]; k.dataset.key = chars[i]; }
|
|
850
|
+
});
|
|
746
851
|
}
|
|
747
852
|
|
|
748
|
-
|
|
749
|
-
|
|
853
|
+
scrambleKeyboard() {
|
|
854
|
+
const c = "1234567890-=+%/*.()".split(""); this.shuffleArray(c);
|
|
855
|
+
this._scramble(".key:not([data-key='std']):not([data-key='scr']):not([data-key='backspace']):not([data-key='Backspace'])", c);
|
|
856
|
+
}
|
|
857
|
+
scrambleEnglishKeys() {
|
|
858
|
+
const c = "abcdefghijklmnopqrstuvwxyz/.,';\\][`1234567890-=".split(""); this.shuffleArray(c);
|
|
859
|
+
this._scramble(".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 ↹'])", c);
|
|
860
|
+
}
|
|
861
|
+
scrambleThaiKeys() {
|
|
862
|
+
const c = '_ๅ/-ภถุึคตจขชๆไำพะัีรนยบลฃฟหกดเ้่าสวงผปแอิืทมใฝ%+๑๒๓๔ู฿๕๖๗๘๙๐"ฎฑธํ๊ณฯญฐ,ฅฤฆฏโฌ็๋ษศซ.ฦฬฒ?์ฺฮฉ)('.split(""); this.shuffleArray(c);
|
|
863
|
+
this._scramble(".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 ↹'])", c);
|
|
864
|
+
}
|
|
865
|
+
scrambleSymbols() {
|
|
866
|
+
const c = "@#$%^&*()_+~`{}|\\:'<>?/[]±§¶!€£¥¢©®™℅‰†".split(""); this.shuffleArray(c);
|
|
867
|
+
this._scramble(".key:not([data-key='std']):not([data-key='scr']):not([data-key='backspace']):not([data-key='Backspace'])", c);
|
|
868
|
+
}
|
|
869
|
+
scrambleFullLayoutKeys() {
|
|
870
|
+
const c = "abcdefghijklmnopqrstuvwxyz1234567890;'\\/][`,.-=".split(""); this.shuffleArray(c);
|
|
871
|
+
this._scramble(".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 ↹'])", c);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
//caps/shift
|
|
750
875
|
_updateAllKeyContent(isActive) {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const layout = layoutMap[this.currentLayout];
|
|
757
|
-
|
|
758
|
-
document.querySelectorAll(".key").forEach((key) => {
|
|
759
|
-
const isLetter =
|
|
760
|
-
key.dataset.key &&
|
|
761
|
-
key.dataset.key.length === 1 &&
|
|
762
|
-
/[a-zA-Zก-๙]/.test(key.dataset.key);
|
|
763
|
-
|
|
764
|
-
if (isLetter) {
|
|
765
|
-
key.textContent = isActive
|
|
766
|
-
? key.dataset.key.toUpperCase()
|
|
767
|
-
: key.dataset.key.toLowerCase();
|
|
768
|
-
}
|
|
876
|
+
const layoutMap = {
|
|
877
|
+
th: this.ThaiAlphabetShift,
|
|
878
|
+
en: this.EngAlphabetShift,
|
|
879
|
+
full: this.FullAlphabetShift
|
|
880
|
+
};
|
|
769
881
|
|
|
770
|
-
|
|
882
|
+
const layout = layoutMap[this.currentLayout];
|
|
771
883
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
884
|
+
this._qAll(".key").forEach((key) => {
|
|
885
|
+
const dk = key.dataset.originalKey || key.dataset.key;
|
|
886
|
+
if (!dk) return;
|
|
887
|
+
|
|
888
|
+
key.dataset.originalKey = dk;
|
|
889
|
+
|
|
890
|
+
if (this.currentLayout === "full" && key.classList.contains("concat-keys")) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (dk.length === 1 && /[a-zA-Z]/.test(dk)) {
|
|
895
|
+
key.textContent = isActive ? dk.toUpperCase() : dk.toLowerCase();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (!layout) return;
|
|
899
|
+
|
|
900
|
+
const cur = key.textContent.trim();
|
|
901
|
+
|
|
902
|
+
if (isActive && layout[cur]) {
|
|
903
|
+
key.textContent = layout[cur];
|
|
904
|
+
key.dataset.key = layout[cur];
|
|
905
|
+
} else if (!isActive) {
|
|
906
|
+
const orig = Object.keys(layout).find((k) => layout[k] === cur);
|
|
907
|
+
if (orig) {
|
|
908
|
+
key.textContent = orig;
|
|
909
|
+
key.dataset.key = orig;
|
|
784
910
|
}
|
|
785
|
-
}
|
|
786
|
-
}
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
}
|
|
787
914
|
|
|
788
915
|
toggleCapsLock() {
|
|
789
916
|
this.capsLockActive = !this.capsLockActive;
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
key.classList.toggle("bg-gray-400", this.capsLockActive);
|
|
917
|
+
this._qAll('.key[data-key="Caps 🄰"]').forEach((k) => {
|
|
918
|
+
k.classList.toggle("active", this.capsLockActive);
|
|
919
|
+
k.classList.toggle("bg-gray-400", this.capsLockActive);
|
|
794
920
|
});
|
|
795
|
-
|
|
796
|
-
// FIX BUG-1: ใช้ _updateAllKeyContent แทน updateKeyContent ที่ซ้ำกัน
|
|
797
921
|
this._updateAllKeyContent(this.capsLockActive);
|
|
798
922
|
}
|
|
799
923
|
|
|
800
924
|
toggleShift() {
|
|
801
|
-
if (this.capsLockActive)
|
|
802
|
-
return this.toggleCapsLock();
|
|
803
|
-
}
|
|
804
|
-
|
|
925
|
+
if (this.capsLockActive) return this.toggleCapsLock();
|
|
805
926
|
this.shiftActive = !this.shiftActive;
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
key.classList.toggle("bg-gray-400", this.shiftActive);
|
|
927
|
+
this._qAll('.key[data-key="Shift ⇧"]').forEach((k) => {
|
|
928
|
+
k.classList.toggle("active", this.shiftActive);
|
|
929
|
+
k.classList.toggle("bg-gray-400", this.shiftActive);
|
|
810
930
|
});
|
|
811
|
-
|
|
812
|
-
// FIX BUG-1: ใช้ _updateAllKeyContent แทน updateKeyContent ที่ซ้ำกัน
|
|
813
931
|
this._updateAllKeyContent(this.shiftActive);
|
|
814
932
|
}
|
|
815
933
|
|
|
934
|
+
//shift maps
|
|
816
935
|
EngAlphabetShift = {
|
|
817
|
-
"`":
|
|
818
|
-
|
|
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
|
-
"/": "?",
|
|
936
|
+
"`":"~","1":"!","2":"@","3":"#","4":"$","5":"%","6":"^","7":"&","8":"*",
|
|
937
|
+
"9":"(","0":")","-":"_","=":"+","[":"{","]":"}","\\":"|",";":":","'":'"',",":"<",".":">","/":"?",
|
|
838
938
|
};
|
|
839
|
-
|
|
840
939
|
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
|
-
ฝ: "ฦ",
|
|
940
|
+
_:"%",ๅ:"+","/":"๑","-":"๒",ภ:"๓",ถ:"๔","ุ":"ู","ึ":"฿",ค:"๕",ต:"๖",จ:"๗",ข:"๘",ช:"๙",ๆ:"๐",ไ:'"',
|
|
941
|
+
ำ:"ฎ",พ:"ฑ",ะ:"ธ","ั":"ํ","ี":"๋",ร:"ณ",น:"ฯ",ย:"ญ",บ:"ฐ",ล:",",ฃ:"ฅ",ฟ:"ฤ",ห:"ฆ",ก:"ฏ",ด:"โ",เ:"ฌ",
|
|
942
|
+
"้":"็","่":"๋",า:"ษ",ส:"ศ",ว:"ซ",ง:".",ผ:"(",ป:")",แ:"ฉ",อ:"ฮ","ิ":"ฺ","ื":"์",ท:"?",ม:"ฒ",ใ:"ฬ",ฝ:"ฦ",
|
|
888
943
|
};
|
|
889
|
-
|
|
890
944
|
FullAlphabetShift = {
|
|
891
|
-
"
|
|
892
|
-
"]":
|
|
893
|
-
"\\": "|",
|
|
894
|
-
";": ":",
|
|
895
|
-
"'": '"',
|
|
896
|
-
",": "<",
|
|
897
|
-
".": ">",
|
|
898
|
-
"/": "?",
|
|
945
|
+
"`":"~","1":"!","2":"@","3":"#","4":"$","5":"%","6":"^","7":"&","8":"*",
|
|
946
|
+
"9":"(","0":")","-":"_","=":"+","[":"{","]":"}","\\":"|",";":":","'":'"',",":"<",".":">","/":"?",
|
|
899
947
|
};
|
|
900
948
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
this.render();
|
|
904
|
-
}
|
|
949
|
+
//layout
|
|
950
|
+
toggle() { this.isVisible = !this.isVisible; this.render(); }
|
|
905
951
|
|
|
906
|
-
// FIX WARN-1: reset isScrambled และ shift/caps เมื่อเปลี่ยน layout
|
|
907
952
|
changeLayout(layout) {
|
|
908
953
|
this.currentLayout = layout;
|
|
909
|
-
this.isScrambled = false;
|
|
910
|
-
this.shiftActive = false;
|
|
911
|
-
this.capsLockActive = false;
|
|
954
|
+
this.isScrambled = false; this.shiftActive = false; this.capsLockActive = false;
|
|
912
955
|
this.render();
|
|
913
956
|
}
|
|
914
957
|
|
|
915
|
-
|
|
916
|
-
for (let i = array.length - 1; i > 0; i--) {
|
|
917
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
918
|
-
[array[i], array[j]] = [array[j], array[i]];
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
// FIX BUG-7: แก้ CSS selector — เพิ่มวงเล็บปิด ) และ fix data-key case
|
|
923
|
-
scrambleKeyboard() {
|
|
924
|
-
const keys = document.querySelectorAll(
|
|
925
|
-
":not([data-key='std']):not([data-key='scr']).key:not([data-key='backspace']):not([data-key='Backspace'])",
|
|
926
|
-
);
|
|
927
|
-
const numbers = "1234567890-=+%/*.()".split("");
|
|
928
|
-
this.shuffleArray(numbers);
|
|
929
|
-
keys.forEach((key, index) => {
|
|
930
|
-
if (index < numbers.length) {
|
|
931
|
-
key.textContent = numbers[index];
|
|
932
|
-
key.dataset.key = numbers[index];
|
|
933
|
-
}
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
scrambleEnglishKeys() {
|
|
938
|
-
const keys = document.querySelectorAll(
|
|
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 ↹'])",
|
|
940
|
-
);
|
|
941
|
-
const englishAlphabet =
|
|
942
|
-
"abcdefghijklmnopqrstuvwxyz/.,';\\][`1234567890-=".split("");
|
|
943
|
-
this.shuffleArray(englishAlphabet);
|
|
944
|
-
keys.forEach((key, index) => {
|
|
945
|
-
if (index < englishAlphabet.length) {
|
|
946
|
-
key.textContent = englishAlphabet[index];
|
|
947
|
-
key.dataset.key = englishAlphabet[index];
|
|
948
|
-
}
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
scrambleThaiKeys() {
|
|
953
|
-
const keys = document.querySelectorAll(
|
|
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 ↹'])",
|
|
955
|
-
);
|
|
956
|
-
const ThaiAlphabet =
|
|
957
|
-
'_ๅ/-ภถุึคตจขชๆไำพะัีรนยบลฃฟหกดเ้่าสวงผปแอิืทมใฝ%+๑๒๓๔ู฿๕๖๗๘๙๐"ฎฑธํ๊ณฯญฐ,ฅฤฆฏโฌ็๋ษศซ.ฦฬฒ?์ฺฮฉ)('.split(
|
|
958
|
-
"",
|
|
959
|
-
);
|
|
960
|
-
this.shuffleArray(ThaiAlphabet);
|
|
961
|
-
keys.forEach((key, index) => {
|
|
962
|
-
if (index < ThaiAlphabet.length) {
|
|
963
|
-
key.textContent = ThaiAlphabet[index];
|
|
964
|
-
key.dataset.key = ThaiAlphabet[index];
|
|
965
|
-
}
|
|
966
|
-
});
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
// FIX BUG-7: แก้ CSS selector ของ scrambleSymbols
|
|
970
|
-
scrambleSymbols() {
|
|
971
|
-
const keys = document.querySelectorAll(
|
|
972
|
-
":not([data-key='std']):not([data-key='scr']).key:not([data-key='backspace']):not([data-key='Backspace'])",
|
|
973
|
-
);
|
|
974
|
-
const symbols = "@#$%^&*()_+~`{}|\\:'<>?/[]±§¶!€£¥¢©®™℅‰†".split("");
|
|
975
|
-
this.shuffleArray(symbols);
|
|
976
|
-
keys.forEach((key, index) => {
|
|
977
|
-
if (index < symbols.length) {
|
|
978
|
-
key.textContent = symbols[index];
|
|
979
|
-
key.dataset.key = symbols[index];
|
|
980
|
-
}
|
|
981
|
-
});
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
scrambleFullLayoutKeys() {
|
|
985
|
-
const keys = document.querySelectorAll(
|
|
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 ↹'])",
|
|
987
|
-
);
|
|
988
|
-
const englishChars =
|
|
989
|
-
"abcdefghijklmnopqrstuvwxyz1234567890;'\\/][`,.-=".split("");
|
|
990
|
-
this.shuffleArray(englishChars);
|
|
991
|
-
keys.forEach((key, index) => {
|
|
992
|
-
if (index < englishChars.length) {
|
|
993
|
-
key.textContent = englishChars[index];
|
|
994
|
-
key.dataset.key = englishChars[index];
|
|
995
|
-
}
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// FIX BUG-2: เก็บ bound reference และ cleanup ให้ถูกต้อง
|
|
958
|
+
//drag
|
|
1000
959
|
startDrag(event) {
|
|
1001
960
|
this.isDragging = true;
|
|
1002
|
-
this.
|
|
1003
|
-
|
|
1004
|
-
this.offsetY =
|
|
1005
|
-
event.clientY - document.getElementById("keyboard").offsetTop;
|
|
1006
|
-
|
|
961
|
+
const kb = this._q("#keyboard");
|
|
962
|
+
this.offsetX = event.clientX - (kb?.offsetLeft ?? 0);
|
|
963
|
+
this.offsetY = event.clientY - (kb?.offsetTop ?? 0);
|
|
1007
964
|
document.addEventListener("mousemove", this._boundDrag);
|
|
1008
965
|
document.addEventListener("mouseup", this._boundStopDrag);
|
|
1009
966
|
}
|
|
1010
|
-
|
|
1011
967
|
stopDrag() {
|
|
1012
968
|
this.isDragging = false;
|
|
1013
969
|
document.removeEventListener("mousemove", this._boundDrag);
|
|
1014
970
|
document.removeEventListener("mouseup", this._boundStopDrag);
|
|
1015
971
|
}
|
|
1016
|
-
|
|
1017
972
|
drag(event) {
|
|
1018
|
-
if (this.isDragging)
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
}
|
|
973
|
+
if (!this.isDragging) return;
|
|
974
|
+
const kb = this._q("#keyboard");
|
|
975
|
+
if (!kb) return;
|
|
976
|
+
kb.style.left = `${event.clientX - this.offsetX}px`;
|
|
977
|
+
kb.style.top = `${event.clientY - this.offsetY}px`;
|
|
1023
978
|
}
|
|
1024
|
-
}
|
|
979
|
+
}
|