// ==UserScript==
// @name 学习通键盘输入模拟器
// @namespace https://github.com/GeLith/keyboard-simulator-web
// @version 1.2.3.5
// @description 模拟键盘输入,解决网页不支持粘贴的问题。支持学习通 UEditor。
// @author GeLith
// @match *://*/*
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// @icon https://felixdd.top/img/1.ico
// @homepage https://felixdd.top/keyboard.html
// @supportURL https://github.com/GeLith/keyboard-simulator-web/issues
// ==/UserScript==
(function() {
'use strict';
var isTyping = false;
var shouldStop = false;
var ball = null;
var panel = null;
var VERSION = '1.2.3';
var isTopFrame = (window.top === window);
if (!isTopFrame) return;
var style = document.createElement('style');
style.textContent = '#ks-ball{position:fixed;top:20px;right:20px;width:44px;height:44px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);border-radius:50%;box-shadow:0 4px 15px rgba(102,126,234,0.5);z-index:9999999;cursor:pointer;display:flex;align-items:center;justify-content:center;user-select:none;transition:transform .2s ease,box-shadow .2s ease;border:2px solid rgba(255,255,255,0.3)}#ks-ball:hover{transform:scale(1.1);box-shadow:0 6px 25px rgba(102,126,234,0.7)}#ks-ball:active{transform:scale(0.95)}#ks-ball-icon{font-size:20px;color:#fff;line-height:1;pointer-events:none;filter:drop-shadow(0 1px 2px rgba(0,0,0,0.2))}#keyboard-simulator-panel{position:fixed;top:80px;right:20px;width:320px;background:#fff;border-radius:10px;box-shadow:0 6px 30px rgba(0,0,0,0.3);z-index:9999998;font-family:"Microsoft YaHei","Segoe UI",sans-serif;font-size:13px;overflow:hidden;border:1px solid #ddd;display:none;animation:ks-fadeIn .2s ease}@keyframes ks-fadeIn{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}#keyboard-simulator-panel.ks-show{display:block}#ks-panel-header{display:flex;align-items:center;gap:8px;padding:10px 14px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;user-select:none;cursor:move}#ks-panel-header span:first-child{font-size:16px}#ks-panel-title{flex:1;font-weight:bold}#ks-close{background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0;line-height:1;width:22px;height:22px;display:flex;align-items:center;justify-content:center;border-radius:50%;transition:background .15s}#ks-close:hover{background:rgba(255,255,255,0.25)}#ks-panel-body{padding:14px}#ks-input{width:100%;height:80px;border:1px solid #ccc;border-radius:4px;padding:8px;font-size:13px;resize:vertical;margin-bottom:12px;font-family:inherit;box-sizing:border-box}#ks-input:focus{outline:none;border-color:#66afe9}.ks-settings{margin-bottom:12px}.ks-row{display:flex;align-items:center;margin-bottom:8px}.ks-row label{width:110px;font-size:12px;color:#555;flex-shrink:0}.ks-row input[type=number]{flex:1;padding:5px 8px;border:1px solid #ccc;border-radius:4px;font-size:13px;box-sizing:border-box}.ks-row input[type=number]:focus{outline:none;border-color:#66afe9}.ks-buttons{display:flex;gap:8px;margin-bottom:10px}.ks-buttons button{flex:1;padding:7px 12px;border:none;border-radius:5px;font-size:13px;cursor:pointer;font-weight:bold;transition:background .15s}#ks-start{background:#5cb85c;color:#fff}#ks-start:hover{background:#449d44}#ks-stop{background:#d9534f;color:#fff}#ks-stop:hover{background:#c9302c}#ks-stop:disabled{background:#ccc;cursor:not-allowed}.ks-btn-clear{width:100%;padding:8px 12px;margin-bottom:10px;background:#f0f0f0;color:#666;border:1px dashed #ccc;border-radius:5px;font-size:13px;cursor:pointer;transition:background .15s,color .15s;box-sizing:border-box}.ks-btn-clear:hover{background:#ffe6e6;color:#d9534f;border-color:#d9534f}#ks-souti{display:block;text-align:center;margin-bottom:10px;font-size:12px;color:#337ab7;text-decoration:none;transition:color .15s}#ks-souti:hover{color:#23527c;text-decoration:underline}#ks-status{font-size:12px;color:#888;text-align:center;padding:5px;background:#f5f5f5;border-radius:4px}#ks-panel-footer{padding:6px 14px;border-top:1px solid #eee;text-align:center;font-size:11px;color:#aaa}#ks-panel-footer a{color:#337ab7;text-decoration:none}#ks-panel-footer a:hover{text-decoration:underline}';
document.head.appendChild(style);
createBall();
function createBall() {
if (ball) return;
ball = document.createElement('div');
ball.id = 'ks-ball';
ball.innerHTML = '⌨';
document.body.appendChild(ball);
ball.addEventListener('mousedown', function(e) {
e.preventDefault();
var sx = e.clientX, sy = e.clientY, st = ball.offsetTop, sl = ball.offsetLeft, mv = false;
function mm(ev) { if (Math.abs(ev.clientX - sx) > 3 || Math.abs(ev.clientY - sy) > 3) mv = true; if (mv) { ball.style.transition = 'none'; ball.style.right = 'auto'; ball.style.top = (st + ev.clientY - sy) + 'px'; ball.style.left = (sl + ev.clientX - sx) + 'px'; } }
function mu() { document.removeEventListener('mousemove', mm); document.removeEventListener('mouseup', mu); if (!mv) togglePanel(); }
document.addEventListener('mousemove', mm); document.addEventListener('mouseup', mu);
});
}
function togglePanel() { if (!panel) createPanel(); panel.classList.toggle('ks-show'); }
function createPanel() {
if (panel) return;
panel = document.createElement('div');
panel.id = 'keyboard-simulator-panel';
panel.innerHTML = [
'
',
'',
'
',
'
',
'
',
'
',
'
一键安装大学搜题酱',
'
就绪 - 点击球图标可收起
',
'
',
''
].join('');
document.body.appendChild(panel);
var savedDelay = GM_getValue('ks-delay', '2');
var savedInterval = GM_getValue('ks-interval', '0.05');
document.getElementById('ks-delay').value = savedDelay;
document.getElementById('ks-interval').value = savedInterval;
document.getElementById('ks-delay').addEventListener('change', function() { GM_setValue('ks-delay', this.value); });
document.getElementById('ks-interval').addEventListener('change', function() { GM_setValue('ks-interval', this.value); });
document.getElementById('ks-close').addEventListener('click', function() { panel.classList.remove('ks-show'); });
var hdr = document.getElementById('ks-panel-header');
hdr.style.cursor = 'move';
hdr.addEventListener('mousedown', function(e) {
if (e.target.id === 'ks-close') return; e.preventDefault();
var sx = e.clientX, sy = e.clientY, st = panel.offsetTop, sl = panel.offsetLeft, mv = false;
function mv2(ev) { if (Math.abs(ev.clientX - sx) > 3 || Math.abs(ev.clientY - sy) > 3) mv = true; if (mv) { panel.style.transition = 'none'; panel.style.right = 'auto'; panel.style.top = (st + ev.clientY - sy) + 'px'; panel.style.left = (sl + ev.clientX - sx) + 'px'; } }
function up() { document.removeEventListener('mousemove', mv2); document.removeEventListener('mouseup', up); }
document.addEventListener('mousemove', mv2); document.addEventListener('mouseup', up);
});
document.getElementById('ks-start').addEventListener('click', function() {
var txt = document.getElementById('ks-input').value;
if (!txt.trim()) { document.getElementById('ks-status').textContent = '请输入文字'; return; }
isTyping = true; shouldStop = false;
document.getElementById('ks-start').disabled = true;
document.getElementById('ks-stop').disabled = false;
document.getElementById('ks-status').textContent = '准备输入...';
startTyping({ text: txt, startDelay: parseFloat(document.getElementById('ks-delay').value) * 1000, charInterval: parseFloat(document.getElementById('ks-interval').value) * 1000 });
});
document.getElementById('ks-stop').addEventListener('click', function() { shouldStop = true; document.getElementById('ks-status').textContent = '正在停止...'; });
document.getElementById('ks-clear').addEventListener('click', function() { document.getElementById('ks-input').value = ''; document.getElementById('ks-input').focus(); });
}
function findUeditorIframe() {
var iframes = document.querySelectorAll('iframe');
for (var i = 0; i < iframes.length; i++) {
try {
var doc = iframes[i].contentDocument;
if (!doc || !doc.body) continue;
if (doc.body.getAttribute('contenteditable') === 'true' || doc.querySelector('.edui-body-container')) {
return iframes[i];
}
} catch(e) {}
}
return null;
}
function startTyping(settings) {
var ueFrame = findUeditorIframe();
if (ueFrame) {
typeViaExecCommand(ueFrame, settings);
} else {
typeLocally(settings);
}
}
function typeViaExecCommand(iframe, settings) {
var doc = iframe.contentDocument;
var body = doc.body;
body.focus();
var statusEl = document.getElementById('ks-status');
async function run() {
await sleep(settings.startDelay);
for (var i = 0; i < settings.text.length; i++) {
if (shouldStop) { finish('已停止'); return; }
if (statusEl) statusEl.textContent = '正在输入 (' + (i + 1) + '/' + settings.text.length + ')...';
var char = settings.text[i];
body.focus();
var sel = doc.getSelection();
sel.collapse(body, body.childNodes.length);
doc.execCommand('insertText', false, char);
if (i < settings.text.length - 1) await sleep(settings.charInterval);
}
finish('输入完成');
}
function finish(msg) {
if (statusEl) { statusEl.textContent = msg; document.getElementById('ks-start').disabled = false; document.getElementById('ks-stop').disabled = true; isTyping = false; }
}
run();
}
function typeLocally(settings) {
var statusEl = document.getElementById('ks-status');
async function run() {
await sleep(settings.startDelay);
for (var i = 0; i < settings.text.length; i++) {
if (shouldStop) { if (statusEl) { statusEl.textContent = '已停止'; document.getElementById('ks-start').disabled = false; document.getElementById('ks-stop').disabled = true; isTyping = false; } return; }
if (statusEl) statusEl.textContent = '正在输入 (' + (i + 1) + '/' + settings.text.length + ')...';
var char = settings.text[i];
var active = document.activeElement;
if (active) {
var tag = active.tagName ? active.tagName.toLowerCase() : '';
if (tag === 'input' || tag === 'textarea') {
var start = active.selectionStart || active.value.length;
active.value = active.value.substring(0, start) + char + active.value.substring(active.selectionEnd || start);
active.selectionStart = active.selectionEnd = start + 1;
active.dispatchEvent(new Event('input', { bubbles: true }));
} else {
active.focus();
try { document.execCommand('insertText', false, char); } catch(e) {}
}
} else {
try { document.execCommand('insertText', false, char); } catch(e) {}
}
if (i < settings.text.length - 1) await sleep(settings.charInterval);
}
if (statusEl) { statusEl.textContent = '输入完成'; document.getElementById('ks-start').disabled = false; document.getElementById('ks-stop').disabled = true; isTyping = false; }
}
run();
}
function sleep(ms) { return new Promise(function(r) { setTimeout(r, ms); }); }
})();