// vim:foldmethod=marker /* ---------------------------------------------------------------------------- Online Moodle/Elearning/KMOOC test help GitLab: 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 . ------------------------------------------------------------------------- */ // : 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, [`
`]) 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, '
') 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