moodle-test-userscript/stable.user.js

644 lines
21 KiB
JavaScript
Executable file

// vim:foldmethod=marker
/* ----------------------------------------------------------------------------
Online Moodle/Elearning/KMOOC test help
GitLab: <https://gitlab.com/MrFry/moodle-test-userscript>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
------------------------------------------------------------------------- */
// : Script header {{{
// ==UserScript==
// @name Moodle/Elearning/KMOOC test help
// @version 9.9.9.9
// @description Online Moodle/Elearning/KMOOC test help
// @author MrFry
// @match https://elearning.uni-obuda.hu/*
// @match https://mooc.unideb.hu/*
// @match https://itc.semmelweis.hu/moodle/*
// @match https://oktatas.mai.kvk.uni-obuda.hu/*
// @match https://qmining.frylabs.net/*
// @match http://qmining.frylabs.net/*
// @noframes
// @run-at document-start
// @grant GM_getResourceText
// @grant GM_info
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @grant unsafeWindow
// @license GNU General Public License v3.0 or later
// @supportURL qmining.frylabs.net
// @contributionURL qmining.frylabs.net
// @namespace https://qmining.frylabs.net
// @updateURL https://qmining.frylabs.net/moodle-test-userscript/stable.user.js?up
// ==/UserScript==
// : }}}
;(function() {
// : ESLINT bs {{{
// eslint-disable-line
// GM functions, only to disable ESLINT errors
/* eslint-disable */
const a = Main
const usf = unsafeWindow
function getVal(name) {
return GM_getValue(name)
}
function setVal(name, val) {
return GM_setValue(name, val)
}
function delVal(name) {
return GM_deleteValue(name)
}
function openInTab(address) {
GM_openInTab(address, false)
}
function xmlhttpRequest(opts) {
GM_xmlhttpRequest(opts)
}
function info() {
return GM_info
}
/* eslint-enable */
// : }}}
// : Constants and global variables {{{
var addEventListener // add event listener function
let serverAdress = 'https://qmining.frylabs.net/'
const messageOpacityDelta = 0.1
const minMessageOpacity = 0.2
// : }}}
// : Stealth by An0 with love {{{
function StealthOverlay() {
//call this before the document scripts
const document = window.document
const neverEqualPlaceholder = Symbol(`never equal`) //block probing for undefined values in the hooks
let shadowRootHost = neverEqualPlaceholder
let shadowRootNewHost = neverEqualPlaceholder
const apply = Reflect.apply //save some things in case they get hooked (only for unsafe contexts)
if (usf.Error.hasOwnProperty('stackTraceLimit')) {
Reflect.defineProperty(usf.Error, 'stackTraceLimit', {
value: undefined,
writable: false,
enumerable: false,
configurable: false,
})
}
const shadowGetHandler = {
apply: (target, thisArg, argumentsList) =>
apply(
target,
thisArg === shadowRootHost ? shadowRootNewHost : thisArg,
argumentsList
),
}
const original_attachShadow = usf.Element.prototype.attachShadow
const attachShadowProxy = new Proxy(original_attachShadow, shadowGetHandler)
usf.Element.prototype.attachShadow = attachShadowProxy
const getShadowRootProxy = new Proxy(
Object.getOwnPropertyDescriptor(usf.Element.prototype, 'shadowRoot').get,
shadowGetHandler
)
Object.defineProperty(usf.Element.prototype, 'shadowRoot', {
get: getShadowRootProxy,
})
const getHostHandler = {
apply: function() {
let result = apply(...arguments)
return result === shadowRootNewHost ? shadowRootHost : result
},
}
const getHostProxy = new Proxy(
Object.getOwnPropertyDescriptor(usf.ShadowRoot.prototype, 'host').get,
getHostHandler
)
Object.defineProperty(usf.ShadowRoot.prototype, 'host', {
get: getHostProxy,
})
const shadowRootSetInnerHtml = Object.getOwnPropertyDescriptor(
ShadowRoot.prototype,
'innerHTML'
).set
const documentFragmentGetChildren = Object.getOwnPropertyDescriptor(
DocumentFragment.prototype,
'children'
).get
const documentGetBody = Object.getOwnPropertyDescriptor(
Document.prototype,
'body'
).get
const nodeAppendChild = Node.prototype.appendChild
const overlay = document.createElement('div')
overlay.style.cssText = 'position:absolute;left:0;top:0'
const addOverlay = () => {
shadowRootHost = apply(documentGetBody, document, [])
const shadowRoot = apply(original_attachShadow, shadowRootHost, [
{ mode: 'closed' },
])
apply(shadowRootSetInnerHtml, shadowRoot, [`<div><slot></slot></div>`])
shadowRootNewHost = apply(documentFragmentGetChildren, shadowRoot, [])[0]
apply(nodeAppendChild, shadowRoot, [overlay])
}
if (!document.body) {
document.addEventListener('DOMContentLoaded', addOverlay)
} else {
addOverlay()
}
return overlay
}
const overlay = StealthOverlay()
// : }}}
Main()
function Main() {
'use strict'
try {
addEventListener = (function() {
if (document.addEventListener) {
return function(element, event, handler) {
element.addEventListener(event, handler, false)
}
} else {
return function(element, event, handler) {
element.attachEvent('on' + event, handler)
}
}
})()
} catch (e) {
console.log(e)
}
ShowMessage(
'Ha ezt az üzeneted látod, akkor a scripted a greasyforkról frissült. Weboldalról is lehet telepíteni a scriptet, és hogy a greasyfork ne legyen egy felesleges kerülő, így innentől ott megszűnt a script. Kattints erre az üzenetre a weboldalról való script telepítés útmutatóhoz.',
undefined,
() => {
openInTab(serverAdress + 'manual.html#scriptreinstall')
}
)
}
function getConvertedMessageNode(message) {
const messageNode = document.createElement('p')
const resultNode = document.createElement('p')
messageNode.innerHTML = message.replace(/\n/g, '</br>')
Array.from(messageNode.childNodes).forEach(node => {
if (node.tagName === 'A') {
let linkNode = document.createElement('span')
SetStyle(linkNode, {
color: 'lightblue',
textDecoration: 'underline',
cursor: 'pointer',
})
linkNode.innerText = node.innerText
linkNode.addEventListener('mousedown', e => {
e.stopPropagation()
openInTab(node.href, {
active: true,
})
})
resultNode.appendChild(linkNode)
} else {
resultNode.appendChild(node)
}
})
return resultNode
}
function addOpacityChangeEvent(elem) {
if (!elem.id) {
console.warn('element must have ID to add opacity change event!')
return
}
let currOpacity = getVal(`${elem.id}_opacity`) || 1
elem.addEventListener('wheel', e => {
e.preventDefault()
const isUp = e.deltaY < 0
if (isUp) {
if (currOpacity + messageOpacityDelta <= 1) {
currOpacity = currOpacity + messageOpacityDelta
}
} else {
if (currOpacity - messageOpacityDelta > minMessageOpacity) {
currOpacity = currOpacity - messageOpacityDelta
}
}
elem.style.opacity = currOpacity
setVal(`${elem.id}_opacity`, currOpacity)
})
}
// shows a message with "msg" text, "matchPercent" tip and transp, and "timeout" time
function ShowMessage(msgItem, timeout, funct) {
// msgItem help:
// [ [ {}{}{}{} ] [ {}{}{} ] ]
// msgItem[] <- a questions stuff
// msgItem[][] <- a questions relevant answers array
// msgItem[][].p <- a questions precent
// msgItem[][].m <- a questions message
try {
let defMargin = '0px 5px'
let isSimpleMessage = false
let simpleMessageText = ''
if (typeof msgItem === 'string') {
simpleMessageText = msgItem
if (simpleMessageText === '') {
// if msg is empty
return
}
msgItem = [
[
{
m: simpleMessageText,
},
],
]
isSimpleMessage = true
}
const appedtTo = overlay // will be appended here
const startFromTop = 25 // top distance
let width = window.innerWidth - window.innerWidth / 6 // with of the box
const mainDiv = document.createElement('div') // the main divider, wich items will be attached to
const id = 'scriptMessage'
mainDiv.setAttribute('id', id)
if (funct) {
addEventListener(mainDiv, 'click', funct)
}
// lotsa crap style
SetStyle(mainDiv, {
position: 'fixed',
zIndex: 999999,
textAlign: 'center',
width: width + 'px',
padding: '0px',
background: '#222d32',
color: '#ffffff',
border: '3px solid #99f',
borderRadius: '5px',
top: startFromTop + 'px',
left: (window.innerWidth - width) / 2 + 'px',
opacity: getVal(`${id}_opacity`),
cursor: funct ? 'pointer' : 'move',
})
// ------------------------------------------------------------------
// transparencity
// ------------------------------------------------------------------
addOpacityChangeEvent(mainDiv)
// ------------------------------------------------------------------
// moving msg
// ------------------------------------------------------------------
const movingEnabled = !funct
let isMouseDown = false
let offset = [0, 0]
let mousePosition
if (movingEnabled) {
mainDiv.addEventListener('mousedown', e => {
isMouseDown = true
offset = [
mainDiv.offsetLeft - e.clientX,
mainDiv.offsetTop - e.clientY,
]
})
mainDiv.addEventListener('mouseup', e => {
isMouseDown = false
})
mainDiv.addEventListener('mousemove', e => {
if (isMouseDown) {
mousePosition = {
x: e.clientX,
y: e.clientY,
}
mainDiv.style.left = mousePosition.x + offset[0] + 'px'
mainDiv.style.top = mousePosition.y + offset[1] + 'px'
}
})
}
const xButton = CreateNodeWithText(null, '❌', 'div')
SetStyle(xButton, {
cursor: 'pointer',
position: 'absolute',
right: '0px',
display: 'inline',
margin: '3px',
})
xButton.addEventListener('mousedown', e => {
e.stopPropagation()
mainDiv.parentNode.removeChild(mainDiv)
})
// ------------------------------------------------------------------
var matchPercent = msgItem[0][0].p
if (isSimpleMessage) {
mainDiv.appendChild(xButton)
var simpleMessageParagrapg = document.createElement('p') // new paragraph
simpleMessageParagrapg.style.margin = defMargin // fancy margin
const mesageNode = getConvertedMessageNode(simpleMessageText)
simpleMessageParagrapg.appendChild(mesageNode)
mesageNode.addEventListener('mousedown', e => {
e.stopPropagation()
})
SetStyle(mesageNode, {
margin: '10px 100px',
cursor: funct ? 'pointer' : 'auto',
})
Array.from(mesageNode.getElementsByTagName('a')).forEach(anchorElem => {
anchorElem.style.color = 'lightblue'
})
mainDiv.appendChild(simpleMessageParagrapg) // adding text box to main div
} else {
const headerRow = document.createElement('div')
headerRow.setAttribute('id', 'msgHeader')
SetStyle(headerRow, {
height: '20px',
userSelect: 'none',
})
mainDiv.appendChild(headerRow)
const headerText = CreateNodeWithText(headerRow, '', 'div')
headerText.title = 'Talált kérdés tárgy neve'
SetStyle(headerText, {
padding: '2px 5px',
textAlign: 'center',
display: 'inline',
})
headerRow.appendChild(xButton)
// if its a fucking complicated message
// TABLE SETUP ------------------------------------------------------------------------------------------------------------
var table = document.createElement('table')
table.style.width = '100%'
// ROWS -----------------------------------------------------------------------------------------------------
var rowOne = table.insertRow() // previous suggestion, question text, and prev question
var rowTwo = table.insertRow() // next question button
var rowThree = table.insertRow() // next suggetsion button
// CELLS -----------------------------------------------------------------------------------------------------
// row one
const sideCellWidth = 30
var numberTextCell = rowOne.insertCell()
SetStyle(numberTextCell, {
width: sideCellWidth + 'px',
})
var questionCell = rowOne.insertCell() // QUESTION CELL
questionCell.setAttribute('id', 'questionCell')
questionCell.rowSpan = 3
var prevQuestionCell = rowOne.insertCell()
SetStyle(prevQuestionCell, {
width: sideCellWidth + 'px',
})
// row two
var percentTextCell = rowTwo.insertCell()
SetStyle(percentTextCell, {
width: sideCellWidth + 'px',
})
var nextQuestionCell = rowTwo.insertCell()
SetStyle(nextQuestionCell, {
width: sideCellWidth + 'px',
})
// row three
var prevSuggestionCell = rowThree.insertCell()
var nextSuggestionCell = rowThree.insertCell()
// adding finally
mainDiv.appendChild(table)
// PERCENT TEXT SETUP -----------------------------------------------------------------------------------------------------
var percentTextBox = CreateNodeWithText(percentTextCell, '')
percentTextBox.setAttribute('id', 'percentTextBox')
percentTextBox.title = 'Egyezés %'
if (matchPercent) {
// if match percent param is not null
percentTextBox.innerText = matchPercent + '%'
}
// NUMBER SETUP -----------------------------------------------------------------------------------------------------
var numberTextBox = CreateNodeWithText(numberTextCell, '1.')
numberTextBox.setAttribute('id', 'numberTextBox')
numberTextBox.title = 'Lehetséges válasz index'
// ANSWER NODE SETUP -------------------------------------------------------------------------------------------------------------
var questionTextElement = CreateNodeWithText(
questionCell,
'ur question goes here, mister OwO'
)
questionTextElement.addEventListener('mousedown', e => {
e.stopPropagation()
})
SetStyle(questionTextElement, {
cursor: funct ? 'pointer' : 'auto',
})
questionTextElement.setAttribute('id', 'questionTextElement')
// BUTTON SETUP -----------------------------------------------------------------------------------------------------------
var currItem = 0
var currRelevantQuestion = 0
const GetRelevantQuestion = () => {
// returns the currItemth questions currRelevantQuestionth relevant question
return msgItem[currItem][currRelevantQuestion]
}
const ChangeCurrItemIndex = to => {
currItem += to
if (currItem < 0) {
currItem = 0
}
if (currItem > msgItem.length - 1) {
currItem = msgItem.length - 1
}
currRelevantQuestion = 0
}
const ChangeCurrRelevantQuestionIndex = to => {
currRelevantQuestion += to
if (currRelevantQuestion < 0) {
currRelevantQuestion = 0
}
if (currRelevantQuestion > msgItem[currItem].length - 1) {
currRelevantQuestion = msgItem[currItem].length - 1
}
}
const SetQuestionText = () => {
const relevantQuestion = GetRelevantQuestion()
headerText.innerText = relevantQuestion.header
questionTextElement.innerText = relevantQuestion.m
if (currItem === 0 && currRelevantQuestion === 0) {
numberTextBox.innerText = currRelevantQuestion + 1 + '.'
} else {
numberTextBox.innerText =
currItem + 1 + './' + (currRelevantQuestion + 1) + '.'
}
percentTextBox.innerText = relevantQuestion.p + '%'
}
const buttonStyle = {
color: 'white',
backgroundColor: 'transparent',
margin: buttonMargin,
border: 'none',
fontSize: '30px',
cursor: 'pointer',
userSelect: 'none',
}
var buttonMargin = '2px 2px 2px 2px' // uniform button margin
if (msgItem[currItem].length > 1) {
// PREV SUGG BUTTON ------------------------------------------------------------------------------------------------------------
var prevSuggButton = CreateNodeWithText(
prevSuggestionCell,
'⬅️',
'div'
)
prevSuggButton.title = 'Előző lehetséges válasz'
SetStyle(prevSuggButton, buttonStyle)
prevSuggButton.addEventListener('mousedown', function(e) {
e.stopPropagation()
ChangeCurrRelevantQuestionIndex(-1)
SetQuestionText()
})
// NEXT SUGG BUTTON ------------------------------------------------------------------------------------------------------------
var nextSuggButton = CreateNodeWithText(
nextSuggestionCell,
'➡️',
'div'
)
nextSuggButton.title = 'Következő lehetséges válasz'
SetStyle(nextSuggButton, buttonStyle)
nextSuggButton.addEventListener('mousedown', function(e) {
e.stopPropagation()
ChangeCurrRelevantQuestionIndex(1)
SetQuestionText()
})
}
// deciding if has multiple questions ------------------------------------------------------------------------------------------------
if (msgItem.length === 1) {
SetQuestionText()
} else {
// if there are multiple items to display
// PREV QUESTION BUTTON ------------------------------------------------------------------------------------------------------------
var prevButton = CreateNodeWithText(prevQuestionCell, '⬆️', 'div')
SetStyle(prevButton, buttonStyle)
prevButton.title = 'Előző kérdés'
// event listener
prevButton.addEventListener('click', function() {
ChangeCurrItemIndex(-1)
SetQuestionText()
})
// NEXT QUESTION BUTTON ------------------------------------------------------------------------------------------------------------
var nextButton = CreateNodeWithText(nextQuestionCell, '⬇️', 'div')
SetStyle(nextButton, buttonStyle)
nextButton.title = 'Előző kérdés'
// event listener
nextButton.addEventListener('click', function() {
ChangeCurrItemIndex(1)
SetQuestionText()
})
SetQuestionText()
}
}
appedtTo.appendChild(mainDiv) // THE FINAL APPEND
// setting some events
// addEventListener(window, 'scroll', function () {
// mainDiv.style.top = (pageYOffset + startFromTop) + 'px';
// })
addEventListener(window, 'resize', function() {
mainDiv.style.left = (window.innerWidth - width) / 2 + 'px'
})
var timeOut
if (timeout && timeout > 0) {
// setting timeout if not zero or null
timeOut = setTimeout(function() {
mainDiv.parentNode.removeChild(mainDiv)
}, timeout * 1000)
}
// middle click close event listener
addEventListener(mainDiv, 'mousedown', function(e) {
if (e.which === 2) {
mainDiv.parentNode.removeChild(mainDiv)
if (timeOut) {
clearTimeout(timeOut)
}
}
})
return {
messageElement: mainDiv,
removeMessage: () => {
mainDiv.parentNode.removeChild(mainDiv)
},
}
} catch (e) {
console.log(e)
}
}
function SetStyle(target, style) {
Object.keys(style)
.sort()
.forEach(key => {
target.style[key] = style[key]
})
}
function CreateNodeWithText(to, text, type) {
var paragraphElement = document.createElement(type || 'p') // new paragraph
var textNode = document.createTextNode(text)
paragraphElement.appendChild(textNode)
if (to) {
to.appendChild(paragraphElement)
}
return paragraphElement
}
})() // eslint-disable-line