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