213 lines
9.2 KiB
JavaScript
213 lines
9.2 KiB
JavaScript
// ==UserScript==
|
||
// @name Submit Click (F12) + Copy CURRENT Visible Question (F10)
|
||
// @namespace vm-submit-click-and-copy-visible-text
|
||
// @match https://*netacad.com/*
|
||
// @require https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1
|
||
// @grant none
|
||
// @version 2.0
|
||
// @run-at document-end
|
||
// ==/UserScript==
|
||
|
||
(function () {
|
||
'use strict';
|
||
|
||
// ────────────────────────────────────────────────
|
||
// Submit button click (F12) – unchanged
|
||
// ────────────────────────────────────────────────
|
||
function findSubmitButton() {
|
||
let btns = document.querySelectorAll('button[aria-label="submit"].submit-button');
|
||
for (const btn of btns) {
|
||
const innerDiv = btn.querySelector('div.submit');
|
||
if (innerDiv && innerDiv.textContent.trim() === 'Submit') return btn;
|
||
}
|
||
|
||
function searchShadows(root) {
|
||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||
let node;
|
||
while ((node = walker.nextNode())) {
|
||
if (node.shadowRoot) {
|
||
const shadowBtns = node.shadowRoot.querySelectorAll('button[aria-label="submit"].submit-button');
|
||
for (const btn of shadowBtns) {
|
||
const innerDiv = btn.querySelector('div.submit');
|
||
if (innerDiv && innerDiv.textContent.trim() === 'Submit') return btn;
|
||
}
|
||
const found = searchShadows(node.shadowRoot);
|
||
if (found) return found;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
return searchShadows(document.body);
|
||
}
|
||
|
||
function triggerSubmitClick() {
|
||
const btn = findSubmitButton();
|
||
if (!btn) return false;
|
||
try { btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); } catch {}
|
||
if (btn.click) try { btn.click(); } catch {}
|
||
return true;
|
||
}
|
||
|
||
// ────────────────────────────────────────────────
|
||
// Copy CURRENT visible question text (F10) – improved
|
||
// ────────────────────────────────────────────────
|
||
function isVisibleAndInView(el) {
|
||
if (!el) return false;
|
||
const style = window.getComputedStyle(el);
|
||
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) < 0.1) return false;
|
||
|
||
let current = el;
|
||
let depth = 0;
|
||
while (current && depth < 15) {
|
||
const s = window.getComputedStyle(current);
|
||
if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) < 0.1) return false;
|
||
current = current.parentElement;
|
||
depth++;
|
||
}
|
||
|
||
const rect = el.getBoundingClientRect();
|
||
if (rect.width < 50 || rect.height < 20) return false; // too tiny for question text
|
||
|
||
// Roughly in viewport
|
||
return rect.top > -50 && rect.bottom < (window.innerHeight + 50) &&
|
||
rect.left > -50 && rect.right < (window.innerWidth + 50);
|
||
}
|
||
|
||
function findCurrentQuestionSpan() {
|
||
console.log('[TextCopy] Searching for CURRENT visible question (strict mode)...');
|
||
|
||
const containerSelectors = [
|
||
'[class*="body-inner"]',
|
||
'.mcq__body-inner',
|
||
'.component__body-inner',
|
||
'[class*="mcq__body"]',
|
||
'[class*="component__body"]'
|
||
];
|
||
|
||
const spanSelectors = ['p span', 'p > span', 'span'];
|
||
|
||
let candidates = [];
|
||
|
||
// Light DOM
|
||
containerSelectors.forEach(sel => {
|
||
document.querySelectorAll(sel).forEach(container => {
|
||
if (!isVisibleAndInView(container)) return;
|
||
spanSelectors.forEach(sSel => {
|
||
container.querySelectorAll(sSel).forEach(span => {
|
||
const txt = span.textContent?.trim();
|
||
if (!txt || txt.length < 20) return;
|
||
if (!isVisibleAndInView(span)) return;
|
||
|
||
const rect = span.getBoundingClientRect();
|
||
const area = rect.width * rect.height;
|
||
const z = parseFloat(window.getComputedStyle(span).zIndex) || 0;
|
||
const hasQuestionMark = txt.includes('?');
|
||
|
||
candidates.push({
|
||
span,
|
||
txt,
|
||
area,
|
||
zIndex: isNaN(z) ? 0 : z,
|
||
hasQuestionMark,
|
||
depth: span.compareDocumentPosition(document.body) // rough DOM order
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
|
||
// Shadow DOM
|
||
function walkShadows(root) {
|
||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||
let node;
|
||
while ((node = walker.nextNode())) {
|
||
if (node.shadowRoot) {
|
||
containerSelectors.forEach(sel => {
|
||
node.shadowRoot.querySelectorAll(sel).forEach(container => {
|
||
if (!isVisibleAndInView(container)) return;
|
||
spanSelectors.forEach(sSel => {
|
||
container.querySelectorAll(sSel).forEach(span => {
|
||
const txt = span.textContent?.trim();
|
||
if (!txt || txt.length < 20) return;
|
||
if (!isVisibleAndInView(span)) return;
|
||
|
||
const rect = span.getBoundingClientRect();
|
||
const area = rect.width * rect.height;
|
||
const z = parseFloat(window.getComputedStyle(span).zIndex) || 0;
|
||
const hasQuestionMark = txt.includes('?');
|
||
|
||
candidates.push({
|
||
span,
|
||
txt,
|
||
area,
|
||
zIndex: isNaN(z) ? 0 : z,
|
||
hasQuestionMark,
|
||
depth: span.compareDocumentPosition(document.body)
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
walkShadows(node.shadowRoot);
|
||
}
|
||
}
|
||
}
|
||
walkShadows(document.body);
|
||
|
||
if (candidates.length === 0) {
|
||
console.log('[TextCopy] No visible candidates found');
|
||
return null;
|
||
}
|
||
|
||
// Sort: prefer question mark → higher z-index → larger area → earlier in DOM
|
||
candidates.sort((a, b) => {
|
||
if (a.hasQuestionMark !== b.hasQuestionMark) return b.hasQuestionMark - a.hasQuestionMark;
|
||
if (a.zIndex !== b.zIndex) return b.zIndex - a.zIndex;
|
||
if (a.area !== b.area) return b.area - a.area;
|
||
return a.depth - b.depth;
|
||
});
|
||
|
||
const best = candidates[0];
|
||
console.log('[TextCopy] Selected:', best.txt.substring(0, 120) + (best.txt.length > 120 ? '...' : ''));
|
||
console.log('[TextCopy] Stats → area:', best.area, '| z-index:', best.zIndex, '| has ?:', best.hasQuestionMark, '| candidates total:', candidates.length);
|
||
|
||
return best.span;
|
||
}
|
||
|
||
async function copyCurrentQuestion() {
|
||
const el = findCurrentQuestionSpan();
|
||
if (!el) return;
|
||
|
||
const text = el.textContent?.trim() || '';
|
||
if (!text) return;
|
||
|
||
console.log('[TextCopy] Copying:', text.substring(0, 120) + (text.length > 120 ? '...' : ''));
|
||
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
console.log('[TextCopy] SUCCESS – current question copied');
|
||
} catch (err) {
|
||
console.error('[TextCopy] Clipboard failed:', err);
|
||
const ta = document.createElement('textarea');
|
||
ta.value = text;
|
||
ta.style.position = 'fixed'; ta.style.opacity = '0';
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(ta);
|
||
console.log('[TextCopy] Fallback copy done');
|
||
}
|
||
}
|
||
|
||
// ────────────────────────────────────────────────
|
||
// Hotkeys
|
||
// ────────────────────────────────────────────────
|
||
VM.shortcut.register('f12', () => {
|
||
setTimeout(() => { if (!triggerSubmitClick()) setTimeout(triggerSubmitClick, 400); }, 60);
|
||
});
|
||
|
||
VM.shortcut.register('f10', copyCurrentQuestion);
|
||
|
||
console.log('[VM-Tools] Loaded v2.0 → F12 = Submit | F10 = Copy CURRENT visible question (strict visibility)');
|
||
})(); |