adding to gitea repo
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
// ==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)');
|
||||
})();
|
||||
Reference in New Issue
Block a user