// ==UserScript==
// @name 学习通键盘输入器
// @namespace https://github.com/GeLith/keyboard-simulator-web
// @version 1.4.2
// @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.4';
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:#fff;border-radius:50%;box-shadow:0 4px 15px rgba(0,0,0,0.2);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(0,0,0,0.08)}#ks-ball:hover{transform:scale(1.1);box-shadow:0 6px 25px rgba(0,0,0,0.3)}#ks-ball:active{transform:scale(0.95)}#ks-ball-icon{font-size:20px;color:#667eea;line-height:1;pointer-events:none}#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:#fff;color:#333;user-select:none;cursor:move}#ks-panel-header span:first-child{font-size:16px;color:#667eea}#ks-panel-title{flex:1;font-weight:bold}#ks-close{background:none;border:none;color:#999;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(0,0,0,0.08)}#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-buttons{display:flex;gap:8px;margin-bottom:8px}.ks-buttons button{flex:1;padding:8px 12px;border:none;border-radius:4px;font-size:13px;cursor:pointer;font-weight:600;transition:background .15s,opacity .15s}#ks-start{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff}#ks-start:hover{opacity:0.9}#ks-start:disabled{opacity:0.5;cursor:not-allowed}#ks-stop{background:#e74c3c;color:#fff}#ks-stop:hover{opacity:0.9}#ks-stop:disabled{opacity:0.5;cursor:not-allowed}.ks-btn-clear{width:100%;padding:6px;border:1px solid #ccc;border-radius:4px;background:#f9f9f9;cursor:pointer;font-size:12px;margin-bottom:8px;transition:background .15s}.ks-btn-clear:hover{background:#eee}#ks-souti{display:block;text-align:center;padding:8px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;text-decoration:none;border-radius:4px;font-size:13px;font-weight:600;margin-bottom:8px;transition:opacity .15s}#ks-souti:hover{opacity:0.9}#ks-status{font-size:12px;color:#888;text-align:center;padding:4px 0}#ks-panel-footer{padding:6px 14px;background:#f5f5f5;text-align:center;font-size:11px;color:#999;border-top:1px solid #eee}#ks-panel-footer a{color:#667eea;text-decoration:none}';
document.head.appendChild(style);
createBall();
var lastUEditorId = null;
function setupEditorFocusTracking() {
var editors = document.querySelectorAll('.edui-editor');
for (var i = 0; i < editors.length; i++) {
var ed = editors[i];
if (ed._ksTracked) continue;
ed._ksTracked = true;
var ifr = ed.querySelector('iframe');
if (!ifr) continue;
try {
var doc = ifr.contentDocument;
if (!doc) continue;
doc.addEventListener('focus', function() {
var qParent = ed.closest('.sub_que_div_parent, .stem_answer');
if (qParent) {
var ta = qParent.querySelector('textarea[id^="answer"]');
if (ta) lastUEditorId = ta.id;
}
}, true);
} catch(e) {}
}
}
var ksObserver = new MutationObserver(function() { setupEditorFocusTracking(); });
ksObserver.observe(document.body, { childList: true, subtree: true });
setupEditorFocusTracking();
function findFocusedEditor() {
var editors = document.querySelectorAll('.edui-editor');
for (var i = 0; i < editors.length; i++) {
var ifr = editors[i].querySelector('iframe');
if (ifr) {
try {
var doc = ifr.contentDocument;
if (doc && doc.hasFocus()) {
var ta = findTextareaByIframe(ifr);
return { iframe: ifr, editorId: ta ? ta.id : 'unknown' };
}
} catch(e) {}
}
}
return null;
}
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', '3');
var savedInterval = GM_getValue('ks-interval', '0.01');
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 = '准备输入...';
if (document.activeElement && document.getElementById('keyboard-simulator-panel').contains(document.activeElement)) {
document.activeElement.blur();
}
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 findEditorByTextareaId(taId) {
var ta = document.getElementById(taId);
if (!ta) return null;
var qParent = ta.closest('.sub_que_div_parent, .stem_answer');
if (qParent) {
var editor = qParent.querySelector('.edui-editor');
if (editor) {
var ifr = editor.querySelector('iframe');
if (ifr) return ifr;
}
}
return null;
}
function findTextareaByIframe(iframe) {
var editor = iframe.closest('.edui-editor');
if (editor) {
var qParent = editor.closest('.sub_que_div_parent, .stem_answer');
if (qParent) {
var ta = qParent.querySelector('textarea[id^="answer"]');
if (ta) return ta;
}
}
return null;
}
function findUeditorIframe() {
var active = document.activeElement;
if (active && active.tagName === 'IFRAME') {
try {
var doc = active.contentDocument;
if (doc && doc.body && (doc.body.getAttribute('contenteditable') === 'true' || doc.querySelector('.edui-body-container'))) {
return active;
}
} catch(e) {}
}
if (active && active.ownerDocument && active.ownerDocument !== document) {
var iframes = document.querySelectorAll('iframe');
for (var i = 0; i < iframes.length; i++) {
try {
if (iframes[i].contentDocument === active.ownerDocument) { return iframes[i]; }
} catch(e) {}
}
}
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 findActiveUEditor() {
var saveBtns = document.querySelectorAll('.saveButtonClass');
for (var i = 0; i < saveBtns.length; i++) {
if (saveBtns[i].offsetWidth > 0 && saveBtns[i].offsetHeight > 0) {
var activeId = saveBtns[i].getAttribute('dataid');
if (!activeId) continue;
var textarea = document.getElementById('answer' + activeId);
if (!textarea) continue;
var qParent = textarea.closest('.sub_que_div_parent, .stem_answer');
if (!qParent) continue;
var editor = qParent.querySelector('.edui-editor');
if (!editor) continue;
var iframe = editor.querySelector('iframe');
if (iframe) return { iframe: iframe, editorId: textarea.id };
}
}
return null;
}
function startTyping(settings) {
var editorFrame = null;
var activeSave = findActiveUEditor();
if (activeSave) {
editorFrame = activeSave.iframe;
}
if (!editorFrame) {
var focused = findFocusedEditor();
if (focused) {
editorFrame = focused.iframe;
}
}
if (!editorFrame && lastUEditorId) {
editorFrame = findEditorByTextareaId(lastUEditorId);
}
if (!editorFrame) {
var activeEl = document.activeElement;
if (activeEl && activeEl.tagName === 'IFRAME') {
var ta = findTextareaByIframe(activeEl);
if (ta) {
editorFrame = activeEl;
}
}
}
if (!editorFrame) {
editorFrame = findUeditorIframe();
}
if (!editorFrame) {
var allIframes = document.querySelectorAll('.edui-editor iframe');
for (var i = allIframes.length - 1; i >= 0; i--) {
try {
var doc = allIframes[i].contentDocument;
if (doc && doc.body && !doc.body.textContent.trim()) {
editorFrame = allIframes[i];
break;
}
} catch(e) {}
}
}
if (editorFrame || document.querySelector('.edui-editor')) {
typeViaExecCommand(editorFrame, settings);
} else {
typeLocally(settings);
}
}
function typeViaExecCommand(initialIframe, settings) {
var statusEl = document.getElementById('ks-status');
async function run() {
await sleep(settings.startDelay);
if (shouldStop) { finish('已停止'); return; }
var iframe = initialIframe;
var focused = findFocusedEditor();
if (focused) {
iframe = focused.iframe;
}
if (!iframe) {
typeLocally(settings);
return;
}
var doc = iframe.contentDocument;
var body = doc.body;
var ta = findTextareaByIframe(iframe);
var targetId = ta ? ta.id : 'unknown';
if (statusEl) statusEl.textContent = '写入: ' + targetId;
body.focus();
var sel = doc.getSelection();
sel.collapse(body, body.childNodes.length);
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];
sel.collapse(body, body.childNodes.length);
if (char === '\n') { doc.execCommand('insertLineBreak'); } else { 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 {
try { if (char === '\n') { document.execCommand('insertLineBreak'); } else { document.execCommand('insertText', false, char); } } catch(e) {}
}
} else {
try { if (char === '\n') { document.execCommand('insertLineBreak'); } else { 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); }); }
})();