MediaWiki:Gadget-quiz.js: Unterschied zwischen den Versionen
AZm87 (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
AZm87 (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
||
Zeile 30: | Zeile 30: | ||
* Felix Riesterer (felix-riesterer.de) | * Felix Riesterer (felix-riesterer.de) | ||
*/ | */ | ||
(function () { | (function () { | ||
Zeile 48: | Zeile 46: | ||
*/ | */ | ||
allQuizzes: [], | allQuizzes: [], | ||
/** | /** | ||
Zeile 668: | Zeile 659: | ||
return 1; | return 1; | ||
}, | }, | ||
Zeile 5.454: | Zeile 5.404: | ||
// start the whole thing | // start the whole thing | ||
q. | q.begin(); | ||
// expose an init function to the window object for a script loader | // expose an init function to the window object for a script loader |
Version vom 6. Januar 2024, 15:38 Uhr
/********************************************************
* R-Quiz - JavaScript-Framework for interactive quizzes *
*********************************************************
*
* V 3.0 (2017/05/01)
*
* This script converts parts of a web page into interactive quizzes.
* In order to achieve that this script searches for certain class names
* which it uses as container elements for a quiz.
* This approach was chosen to enable content editors to design quiz
* exercises with regular rich text editing tools so the text contents
* then can get converted into quizzes by this JavaScript.
*
* SOFTWARE LICENSE: LGPL
* (C) 2007 Felix Riesterer
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Felix Riesterer (felix-riesterer.de)
*/
(function () {
'use strict';
var q = {
/*======================*
* framework properties *
*======================*/
/**
* list of all quizzes
*
* @var Array
*/
allQuizzes: [],
/**
* class names for backwards compatibility with older versions of
* this script
*
* @var Object
*/
compatibilityClassNames: {
"zuordnungs-quiz": "rquiz-matching",
"lueckentext-quiz": "rquiz-gapfill",
"memo-quiz": "rquiz-memo",
"multiplechoice-quiz": "rquiz-multichoice",
"schuettel-quiz": "rquiz-wordjumble",
"kreuzwort-quiz": "rquiz-crossword",
"suchsel-quiz": "rquiz-wordsearch",
"buchstabenraten-quiz": "rquiz-wordguess"
},
/**
* functions to execute after the CSS file has been loaded
*
* @var Array of functions
*/
functionsAfterCssReady: [],
/**
* string messages sorted by language
*
* @var Object
*/
i18n: {
de: {
allFound: "Alle Sets found!",
attemptLastTime: "Das letzte Mal hatten Sie nur einen Versuch benötigt.",
attemptsLastTime: "Das letzte Mal hatten Sie %n Versuche benötigt.",
check: "prüfen!",
endOfQuiz: "Quiz ist zuende.",
enter: "eintragen",
enterNotice: "Benutzen Sie zur Eingabe die Tastatur. Eventuell müssen sie zuerst ein Eingabefeld durch Anklicken aktivieren.",
enterNoticeWordGuessing: "Benutzen Sie die Tastatur zur Eingabe! Eventuell müssen Sie erst in das Quiz klicken, um es zu aktivieren.",
foundWords: "Erkannte Wörter",
guessedChars: "Bereits geratene Buchstaben",
horizontal: "Waagrecht",
howAboutNewRound: "Wie wär's mit einer neuen Runde?",
input: "Eingabe",
percentageResult: "Die Antworten sind zu %n% richtig.",
praise1: "Ausgezeichnet!",
praise2: "Gut gemacht!",
praise3: "Das war nicht schlecht!",
remainingTries: "Sie haben noch %n Versuche übrig!",
restart: "neu starten",
result1: "Die Aufgabe wurde gleich beim ersten Versuch erfolgreich gelöst!",
result2: "Die Aufgabe wurde nach nur zwei Versuchen erfolgreich gelöst!",
result3: "Die Aufgabe wurde nach %n Versuchen erfolgreich gelöst!",
startQuiz: "Quiz starten.",
tempText: "Text...",
vertical: "Senkrecht"
},
en: {
allFound: "You've found all sets!",
attemptLastTime: "Last time you only needed a single attempt.",
attemptsLastTime: "Last time you needed %n attempts.",
check: "check it!",
endOfQuiz: "Quiz is over.",
enter: "fill in",
enterNotice: "Use the keyboard to enter letters. You may need to first activate a box by clicking it.",
enterNoticeWordGuessing: "Use the keyboard to enter letters. You may need to first click somewhere into this quiz in oder to activate it.",
foundWords: "Found Words",
guessedChars: "Already Guessed Characters",
horizontal: "Horizontal",
howAboutNewRound: "How about another round?",
input: "Input",
percentageResult: "The answers are %n% correct.",
praise1: "Brilliant!",
praise2: "Well done!",
praise3: "That was nice!",
remainingTries: "You have %n tries left!",
restart: "restart",
result1: "You solved everything on your first try!",
result2: "You solved everything with only two tries!",
result3: "You solved everything after trying %n times!",
startQuiz: "Start quiz.",
tempText: "text...",
vertical: "Vertical"
},
// Spanish messages by Ulrike Weinmann
es: {
allFound: "¡Encontraste todos los juegos!",
attemptLastTime: "Last time you only needed a single attempt.",
attemptsLastTime: "Last time you needed %n attempts.",
check: "¡Chequear!",
endOfQuiz: "Fin de juego",
enter: "insertar",
enterNotice: "Usa el teclado para entrar letras. Quizás tienes que hacer clic en una caja primero para activárla.",
enterNoticeWordGuessing: "Usa el teclado para entrar letras. Quizás tienes que hacer clic en el quiz primero para activárla.",
foundWords: "Palabras encontradas",
guessedChars: "Letras ya probadas",
howAboutNewRound: "¿Otra vez?",
horizontal: "Horizontal",
input: "Entrada",
percentageResult: "Porcentaje de respuestas correctas: %n%.",
praise1: "¡Muy bien hecho!",
praise2: "¡Bien hecho!",
praise3: "¡Correcto!",
remainingTries: "¡Usted no tiene que dejan 7 intentos!",
restart: "otra vez",
result1: "¡Resolviste el ejercicio al primer intento!",
result2: "¡Resolviste el ejercicio al segundo intento!",
result3: "Intentaste resolver el ejercicio %n veces y ¡lo lograste!",
startQuiz: "Empezar quiz.",
tempText: "Texto...",
vertical: "Vertical"
},
// French messages by Otto Ebert
fr: {
allFound: 'Tu as trouvé tous les "sets".',
attemptLastTime: "Last time you only needed a single attempt.",
attemptsLastTime: "Last time you needed %n attempts.",
check: "verifier!",
endOfQuiz: "Quiz est finis.",
enter: "inscrire",
enterNotice: "Utilisez le clavier pour inscrire des lettres. Vous devez probablement d'abord activer une boîte en le claquant.",
enterNoticeWordGuessing: "Utilisez le clavier pour inscrire des lettres. Vous devez probablement d'abord activer le quiz en le claquant.",
foundWords: "Mots trouvés",
guessedChars: "Lettres déjà essayées",
horizontal: "Horizontal",
howAboutNewRound: "Alors tu veux recommencer?",
input: "Entrée",
percentageResult: "Les réponses sont %n% correctes.",
praise1: "Excellent! Super!",
praise2: "Bien fait!",
praise3: "Ce n'était pas mal",
remainingTries: "Vous n'avez pas %n tentatives gauche!",
restart: "recommencer",
result1: "Ton essai était tout de suite un succès.",
result2: "Tu as résoulu le devoir après deux tentatives seulement!",
result3: "Tu as résoulu le devoir après %n tentatives.",
startQuiz: "Commencer le quiz.",
tempText: "Text...",
vertical: "Vértical"
},
// Roman Latin messages by Ralf Altgeld and Ulrike Weinmann
la: {
allFound: "Omnes partes repperisti.",
attemptLastTime: "Last time you only needed a single attempt.",
attemptsLastTime: "Last time you needed %n attempts.",
check: "probare",
endOfQuiz: "Factum est.",
enter: "complere",
enterNotice: "Utere clavibus ad verba scribenda. Fortasse tibi capsa eligenda est.",
enterNoticeWordGuessing: "Utere clavibus ad verba scribenda. Fortasse tibi aenigma eligendum est.",
foundWords: "Verba iam reperta",
guessedChars: "Litterae iam temptatae",
horizontal: "directe",
howAboutNewRound: "Ludum novum vis?",
input: "implere",
percentageResult: "%n% centesimae responsorum rectae sunt.",
praise1: "optime!",
praise2: "bene!",
praise3: "Id non male fecisti.",
remainingTries: "Et non sunt derelicti %n conatusque prohibebit!",
restart: "novum vis",
result1: "Pensum statim in primo conatu feliciter absolutum est!",
result2: "Pensum cam post duos conatus feliciter absolutum est.",
result3: "Pensum cam post %n conatus feliciter absolutum est.",
startQuiz: "Incipere aenigma.",
tempText: "scriptum...",
vertical: "perpendiculariter"
},
// Italian messages by Ihor Bilaniuk
it: {
allFound: "Tutti i sets sono stati risolti!",
attemptLastTime: "Last time you only needed a single attempt.",
attemptsLastTime: "Last time you needed %n attempts.",
check: "controllare!",
endOfQuiz: "Il quiz è sopra .",
enter: "immettere",
enterNotice: "Utilizzi la tastiera per entrare nelle lettere. Potete avere bisogno di in primo luogo di attivare una scatola scattandola.",
enterNoticeWordGuessing: "Utilizzi la tastiera per entrare nelle lettere. Potete avere bisogno di in primo luogo di scattarti in qualche luogo in questo quiz per attivarlo.",
foundWords: "Parole trovate ",
guessedChars: "Lettere già indovinate",
howAboutNewRound: "Ancora una volta?",
horizontal: "Orizontale",
input: "Input",
percentageResult: "Le tue risposte sono il %n per cento giuste.",
praise1: "Ottimo!",
praise2: "Benissimo!",
praise3: "Bene!",
remainingTries: "Non si dispone di %n tentativi di sinistra!",
restart: "ancora una volta",
result1: "Il compito è stato risolto al primo passo!",
result2: "Il compito è stato risolto dopo la seconda prova!",
result3: "Il compito è stato risolto dopo %n prove.",
startQuiz: "Inizi il quiz.",
tempText: "Testo...",
vertical: "Verticale"
},
// Polish messages by Pitr Wójs www.merula.pl
pl: {
allFound: "Znalazłaś/łeś wszystkie pary!",
attemptLastTime: "Last time you only needed a single attempt.",
attemptsLastTime: "Last time you needed %n attempts.",
check: "Sprawdź!",
endOfQuiz: "Koniec quizu.",
enter: "Wpisz",
enterNotice: "Aby wpisać rozwiązanie użyj klawiatury. Kliknij pole, aby wprowadzić text!",
enterNoticeWordGuessing: "Aby wpisać rozwiązanie użyj klawiatury. Kliknij pole, aby wprowadzić text!",
foundWords: "Rozpoznane słówka",
guessedChars: "Odgadnięte litery",
horizontal: "Poziomo",
howAboutNewRound: "Co powiesz na drugą rundę? Spróbuj jeszcze raz!",
input: "Wprowadzanie",
percentageResult: "Odpowiedzi są poprawne w %n procentach.",
praise1: "Celująco!",
praise2: "Bardzo dobrze!",
praise3: "Nieźle!",
remainingTries: "Nie masz %n% prób w lewo!",
restart: "restart",
result1: "Zadanie rozwiązałaś/łeś poprawnie za pierwszym razem!",
result2: "Zadanie rozwiązałaś/łeś poprawnie za drugim razem!",
result3: "Zadanie zostało rozwiązane poprawnie po %n próbach !",
startQuiz: "Start quizu.",
tempText: "Tekst...",
vertical: "Pionowo"
},
// Turkish messages by Dilan Memili
tr: {
allFound: "Tüm setler bulunmus bulunmaktadir!",
attemptLastTime: "Son seferde yalnizca bir deneme yapmaniz gerekti.",
attemptsLastTime: "Son seferde %n deneme yapmaniz gerekti.",
check: "Kontrol ediniz!",
endOfQuiz: "Test bitmistir.",
enter: "Giriniz",
enterNotice: "Giris icin klavyeyi kullaniniz. Belki öncelikle tiklayarak bir giris alani etkinlestirmeniz gerekmektedir.",
enterNoticeWordGuessing: "Giris icin klavyeyi kullaniniz! Testi etkinlestirmek icin belki öncelikle testi tiklamalisiniz.",
foundWords: "Taninan kelimeler",
guessedChars: "Simdiden bulunan harfler",
horizontal: "Yatay",
howAboutNewRound: "Yeni bir tura ne dersiniz?",
input: "Giris",
percentageResult: "Cevaplar %n% dogru.",
praise1: "Mükemmel!",
praise2: "Iyi yaptiniz!",
praise3: "Bu fena degildi!",
remainingTries: "%n denemeniz kaldi",
restart: "Yeniden baslat",
result1: "Ödev ilk denemede basari ile cözüldü!",
result2: "Ödev yalnizca iki denemeden sonra basari ile cözüldü!",
result3: "Ödev %n denemeden sonra basari ile cözüldü!",
startQuiz: "Testi baslat.",
tempText: "metin...",
vertical: "dikey"
}
},
/**
* flag for the framework initialization
*
* If this flag is set certain changes won't be done again by
* the framework's begin method upon being called yet again.
*
* @var bool
*/
initialized: false,
/**
* list of UTF-8 characters that can be simplified into upper-case
* ASCII as needed with some quizzes like a crossword quiz
*
* @var Object
*/
utf8Replacements: {
A: [
'\u0041', // a
'\u0061', // A
'\u00c0', // À
'\u00c1', // Á
'\u00c2', // Â
'\u00c3', // Ã
'\u00c5', // Å
'\u00e0', // à
'\u00e1', // á
'\u00e2', // â
'\u00e3', // ã
'\u00e5' // å
],
AE: [
'\u00c4', // Ä
'\u00c6', // Æ
'\u00e4', // ä
'\u00e6' // æ
],
B: [
'\u0042', // B
'\u0062' // b
],
C: [
'\u0043', // C
'\u0063', // c
'\u00c7', // Ç
'\u00e7' // ç
],
D: [
'\u0044', // D
'\u0064' // d
],
E: [
'\u0045', // E
'\u0065', // e
'\u00c8', // È
'\u00c9', // É
'\u00ca', // Ê
'\u00cb', // Ë
'\u00e8', // è
'\u00e9', // é
'\u00ea', // ê
'\u00eb' // ë
],
F: [
'\u0046', // F
'\u0066' // f
],
G: [
'\u0047', // G
'\u0067' // g
],
H: [
'\u0048', // H
'\u0068' // h
],
I: [
'\u0049', // I
'\u0069', // i
'\u00cc', // Ì
'\u00cd', // Í
'\u00ce', // Î
'\u00cf', // Ï
'\u00ec', // ì
'\u00ed', // í
'\u00ee', // î
'\u00ef' // ï
],
J: [
'\u004a', // J
'\u006a' // j
],
K: [
'\u004b', // K
'\u006b' // k
],
L: [
'\u004c', // L
'\u006c' // l
],
M: [
'\u004d', // M
'\u006d' // m
],
N: [
'\u004e', // N
'\u006e', // n
'\u00d1', // Ñ
'\u00f1' // ñ
],
O: [
'\u004f', // O
'\u006f', // o
'\u00d2', // Ò
'\u00d3', // Ó
'\u00d4', // Ô
'\u00d5', // Õ
'\u00f2', // ò
'\u00f3', // ó
'\u00f4', // ô
'\u00f5' // õ
],
OE: [
'\u00d6', // Ö
'\u00f6' // ö
],
P: [
'\u0050', // P
'\u0070' // p
],
Q: [
'\u0051', // Q
'\u0071' // q
],
R: [
'\u0052', // R
'\u0072' // r
],
S: [
'\u0053', // S
'\u0073' // s
],
SS: [
'\u00df' // ß
],
T: [
'\u0054', // T
'\u0074' // t
],
U: [
'\u0055', // U
'\u0075', // u
'\u00d9', // Ù
'\u00da', // Ú
'\u00db', // Û
'\u00f9', // ù
'\u00fa', // ú
'\u00fb' // û
],
UE: [
'\u00dc', // Ü
'\u00fc' // ü
],
V: [
'\u0056', // V
'\u0076' // v
],
W: [
'\u0057', // W
'\u0077' // w
],
X: [
'\u0058', // X
'\u0078' // x
],
Y: [
'\u0059', // Y
'\u0079', // y
'\u00dd', // Ý
'\u00fd' // ý
],
Z: [
'\u005a', // Z
'\u007a' // z
]
},
/**
* constructor functions for quiz objects
*
* @var Object
*/
quizConstructors: {},
/*===================*
* framework methods *
*===================*/
/**
* function to start the conversion of container elements into
* real quizzes
*
* This function gets exposed to the browser's window object
* so a script loader may call it from outside this IIFE.
*/
begin: function () {
/* Only do the following if no quiz has been created yet
* in order to deflect multiple calls to this function since
* it will be exposed to the browser's window object.
*/
if (!q.initialized) {
// mark this framework as initialized
q.initialized = true;
// make quiz constructors inherit from an abstract base class
q.each(q.quizConstructors, function (o, s) {
if (s != "_abstractQuiz") {
o.prototype = Object.create(q.quizConstructors._abstractQuiz);
o.prototype.constructor = q.quizConstructors[s];
o.prototype.type = s;
}
});
/* turn all elements into quizzes
* which have a suitable class name */
q.each(
document.querySelectorAll('[class^="rquiz-"], [class$="-quiz"]'),
function (element) {
/* compatibility with older versions of this script:
* replace old class names with corresponding new ones */
q.each(q.compatibilityClassNames, function (_new, _old) {
if (element.classList.contains(_old)) {
element.classList.remove(_old);
element.classList.add(_new);
}
});
// decide if the element can be a quiz container
q.each(q.quizConstructors, function (o, s) {
if (element.classList.contains("rquiz-" + s)
&& s != "_abstractQuiz"
) {
// make a quiz
new q.quizConstructors[s](element);
}
});
}
);
// enable display of quizzes
document.body.classList.add("rquiz");
}
},
/**
* more complex document.createElement functionality
*
* params has this structure: {
* tagName : "p", // results in a <p>
* text : "simple plain text" // textNode as child node
* ... // more (native) properties (like id, className etc.)
* }
*
* @param Object
*/
create: function (params) {
var el, p;
if (params.tagName && params.tagName.match(/[a-z]/)) {
el = document.createElement(params.tagName);
for (p in params) {
if (p.match(/^text/i)) {
el.appendChild(document.createTextNode(params[p]));
} else {
if (!p.match(/^tagname$/i)) {
el[p] = params[p];
}
}
}
}
return el;
},
/**
* do things after the CSS file has loaded
*
* This function should execute when the CSS file has been
* loaded. It then executes any functions that have been
* collected in the frameworks functionsAfterCssReady array.
*/
onCssReady: function () {
q.each(q.functionsAfterCssReady, function (f) {
f();
});
},
/**
* iterator function for more than just arrays
* taken from the TinyMCE project (tinymce.com)
*
* @param Object
* @param function callback
* @param Object this context
* @return int (to be used as false-ish or true-ish)
*/
each: function (o, cb, s) {
var n, l;
if (!o) {
return 0;
}
s = s || o;
if (o.length !== undefined) {
// Indexed arrays, needed for Safari
for (n=0, l = o.length; n < l; n++) {
if (cb.call(s, o[n], n, o) === false) {
return 0;
}
}
} else {
// Hashtables
for (n in o) {
if (o.hasOwnProperty(n)) {
if (cb.call(s, o[n], n, o) === false) {
return 0;
}
}
}
}
return 1;
},
/**
* function to shuffle an array
*
* http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array#answer-25984542
*
* @param Array input
*/
shuffleArray: function (array) {
var count = array.length, r, temp;
while (count) {
r = Math.random() * count-- | 0;
temp = array[count];
array[count] = array[r];
array[r] = temp;
}
},
/**
* function to trim whitespaces at the beginning and at the end
* of a string like PHP's trim function
*
* @param String input
* @return String output
*/
trim: function (s) {
var c = "["
+ String.fromCharCode(32)
+ String.fromCharCode(160)
+ "\t\r\n"
+ "]+";
return s
// remove whitespaces from the left end
.replace(new RegExp("^" + c, "g"), "")
// remove whitespaces from the right end
.replace(new RegExp(c + "$", "g"), "");
},
/**
* function to turn all characters of a UTF-8 encoded string
* into an upper-case ASCII representation as defined in
* q.utf8Replacements above
*
* @param String input
* @return String output
*/
utf8NormalizeToUpper: function (s) {
var r = "",
z, i, j;
for (i = 0; i < s.length; i++) {
if (s[i] == String.fromCharCode(160)
|| s[i] == String.fromCharCode(32)
) {
r += String.fromCharCode(160);
} else {
for (z in q.utf8Replacements) {
if (z.match(/^[A-Z][A-Z]?$/)) {
for (j = 0; j < q.utf8Replacements[z].length; j++) {
if (s.substr(i, 1) == q.utf8Replacements[z][j]) {
r += z;
}
}
}
}
}
}
return r;
}
};
/**
* abstract quiz class
*
* This class provides drag&drop functionality.
*
* @param Object HTMLElement
*/
q.quizConstructors._abstractQuiz = function (element) {
var t = this;
/*=================*
* quiz properties *
*=================*/
/**
* a user's number of attempts to solve the quiz
*
* @param int
*/
t.attempts = 0;
/**
* a quiz's container element
*
* The container element is any block element with a
* suitable class name. The framework's begin method
* will recognize this class name and instantiate a
* quiz object with a reference to this block element
* as argument for the constructor.
*/
t.container = element;
/**
* a quiz's data
*
* We are going to analyze a <table> element for its
* columns. The columns' contents of every row get
* stored in an array. These arrays get stored in the
* this.data array.
*/
t.data = [];
/**
* class name for draggable elements
*
* @var String
*/
t.draggableClass = "rquiz-draggable";
/**
* class name for dragged elements
*
* @var String
*/
t.draggingClass = "rquiz-dragging";
/**
* element to be dragged
*
* @var Object HTMLElement
*/
t.dragElm = null;
/**
* flag for drag element in motion
*
* @var bool
*/
t.dragging = false;
/**
* flag for drag&drop mode
*
* @var bool
*/
t.dragMode = false;
/**
* class name for elements marked as error
*
* @var String
*/
t.errorClass = "rquiz-error";
/**
* class name for the container of a finished quiz
*
* @var String
*/
t.finishedClass = "rquiz-finished";
/**
* class name for "pieces" (data-carrying elements)
*
* @var String
*/
t.highlightClass = "rquiz-highlighted";
/**
* a quiz's language setting
*
* The language setting defaults to "de" because of
* backwards compatibilty with older versions of this
* script. If the container element has a "lang"
* attribute and if its value is supported by the
* framework's i18n property, the quiz's language
* setting will be changed to the container element's
* "lang" attribute value.
*/
t.lang = "de"; // default
/**
* last known mouse or touch coordinates
*
* @var Object
*/
t.lastCoords = {
left: 0,
top: 0
};
/**
* a quiz's pieces
*
* Some quizzes need to store references to data-carrying
* elements. This array is exactly for that purpose.
*/
t.pieces = [];
/**
* class name for "pieces" (data-carrying elements)
*
* @var String
*/
t.piecesClass = "rquiz-piece";
/**
* a quiz's pool element
*
* Some quizzes that use drag&drop need an area where
* the draggable elements are placed. The pool element
* is this very area.
*/
t.pool = document.createElement("p");
/**
* class name for data pools
*
* @var String
*/
t.poolClass = "rquiz-pool";
/**
* class name for elements that are not to be displayed
* on-screen but on paper only
*
* @param String
*/
t.printOnlyClass = "rquiz-print";
/**
* a quiz's name
*
* The quiz's name is relevant when drag&drop elements
* need to get identified as part of this individual
* quiz.
*
* @var String
*/
t.quizName = "rquiz" + q.allQuizzes.length;
/**
* a quiz's restart button
*
* @param Object HTMLElement <button>
*/
t.restartButton = document.createElement("button");
/**
* a quiz's message field element
*
* Every quiz will give feedback on how well a user has
* done. At the end of such a message there will be the
* quiz's restart button to offer the user another go on
* this quiz.
*/
t.result = document.createElement("p");
/**
* a quiz's result button
*
* @param Object HTMLElement <button>
*/
t.resultButton = document.createElement("button");
/**
* class name for the result box
*
* @var String
*/
t.resultClass = "rquiz-result";
/**
* class name for elements that are not to be displayed
* on paper but on screen only
*
* @param String
*/
t.screenOnlyClass = "rquiz-screen";
/**
* class name for drag&drop targets
*
* @var String
*/
t.targetClass = "rquiz-target";
/**
* a quiz's drag&drop target fields
*
* @var Array
*/
t.targets = [];
/**
* class name for the result box when an AJAX call hasn't
* yet returned
*
* @var String
*/
t.waitClass = "rquiz-waiting";
/*==============*
* quiz methods *
*==============*/
/**
* function to create a grid filled with words either
* crossword-style or with diagonally placed words
*
* A "word" is an object with this structure: {
* hint: (String), // optional - used in crossword quizzes
* orientation: (String), // (→|↓|↘|↗)
* original: (String), // optional - used in wordsearch quizzes
* upperCase: (String),
* x: (int),
* y: (int)
* }
*
* If "interlinked" words are expected the function will
* try up to ten times to get a solution where all words
* are interlinked.
*
* If "diagonally" placed words are expected the words
* will be placed in such a way that the reading
* direction will always be from left to right, no
* matter if the beginning is on the upper or lower end.
*
* @param Array the words to be placed within the grid
* @param bool interlink words on a common character ifpossible
* @param bool also try to place words diagonally
* @param bool no empty spaces between words necessary (true for wordsearch quizzes)
*/
t.createWordGrid = function (words, interlinked, diagonally, noEmptySpaces) {
var finished = [], // collect usable grids here
taints = 1,
maxAttempts = interlinked ? 10 : 1,
orientations = (diagonally ? "→↓↘↗" : "→↓").split(""),
a, b, i, failed, fitted, grid, letters,
possibleOrientations, r, test,used, word, x, y;
/* create a grid - if impossible without taints,
* try up to maxAttempts times */
while (taints > 0 && finished.length < maxAttempts) {
taints = 0; // expect a perfect grid without taints
used = []; // no used words so far
failed = []; // no unsuccessfully tried words so far
// begin with a grid of 0x0 fields and ...
test = 0;
// ... expand to maxChars x maxChars
q.each(words, function (w) {
// number of a word's chars +2 as padding
test += w.upperCase.length +2;
});
grid = new Array(test);
for (i = 0; i < test; i++) {
grid[i] = new Array(test);
}
// try to place words into the grid in random order
while (used.length < words.length) {
test = true; // force new random number
while (test) {
r = Math.floor(Math.random() * words.length);
// expect this random number to be unused
test = false;
// word already used?
q.each(used, function (a) {
if (a.upperCase == words[r].upperCase) {
test = true;
}
});
if (failed.length > 0) {
/* exclude words that have been
* tried unsuccessfully */
q.each(failed, function (e) {
if (words[r] == e) {
test = true;
}
});
}
}
// we got an unused word to fit into the grid
word = words[r];
word.x = -1;
word.y = -1;
// find suitable position in the grid
if (used.length > 0) {
/* fit another word
* -> check with already fitted words */
for (a = 0; a < used.length; a++) {
// already found suitable position?
if (word.x >= 0 && word.y >= 0) {
break; // yes! -> stop right here
}
/* randomly choose a letter and look
* for a match with fitted word */
letters = []; // empty used letters
for (b = 0; b < word.upperCase.length; b++) {
// found suitable position?
if (word.x >= 0 && word.y >= 0) {
break; // yes! -> stop right here
}
test = true; // force new random number
while (test) {
r = Math.floor(Math.random() * word.upperCase.length);
test = letters[r];
}
// mark this random letter as used
letters[r] = true;
// both words contain this letter?
if (used[a].upperCase.indexOf(
word.upperCase.substr(r, 1)
) >= 0
) {
// yes! -> check every letter of fitted word
for (
fitted = 0;
fitted < used[a].upperCase.length;
fitted++
) {
// word already successfully fitted?
if (word.x >= 0 && word.y >= 0) {
break; // yes! -> stop right here
}
if (used[a].upperCase.substr(fitted, 1)
== word.upperCase.substr(r, 1)
) {
/* determined common letter
* -> test-fit word into grid */
test = true; // expect success
/* determine position
* of the letter inside the grid */
if (used[a].orientation == "→") {
x = used[a].x + fitted;
y = used[a].y;
}
if (used[a].orientation == "↓") {
x = used[a].x;
y = used[a].y + fitted;
}
if (used[a].orientation == "↘") {
x = used[a].x + fitted;
y = used[a].y + fitted;
}
if (used[a].orientation == "↗") {
x = used[a].x + fitted;
y = used[a].y - fitted;
}
/* determine possible
* orientations for the word */
possibleOrientations = [];
for (
i = 0;
i < orientations.length;
i++
) {
if (orientations[i]
!= used[a].orientation
) {
possibleOrientations.push(
orientations[i]
);
}
}
q.shuffleArray(possibleOrientations);
while (possibleOrientations.length) {
// decide direction
word.orientation = possibleOrientations.pop();
/* determine start position
* of the word inside the grid
* with r being the distance
* between current letter and
* initial letter of the word */
if (word.orientation == "↓") {
y = y - r;
}
if (word.orientation == "→") {
x = x - r;
}
if (word.orientation == "↘") {
x = x - r;
y = y - r;
}
if (word.orientation == "↗") {
x = x - r;
y = y + r;
}
// check if needed fields are already occupied
for (
i = 0;
i < word.upperCase.length;
i++
) {
if (word.orientation == "→") {
if (grid[y][x + i]
&& grid[y][x + i]
!= word.upperCase.substr(i, 1)
) {
test = false;
}
// neighbouring field empty?
if (!noEmptySpaces) {
/* check all used words if they
* run parallel in a neighbouring
* row alongside the current word */
q.each(used, function (a) {
if (a.orientation == "→"
&& (
a.y + 1 == y
|| a.y -1 == y
)
) {
if (
(
// neighbouring word longer?
a.x <= x
&& a.upperCase.x + a.upperCase.length >= x + word.upperCase.length
) || (
// neighbouring word overlaps beginning of current word
a.x <= x
&& a.x + a.upperCase.length >= x
) || (
// neighbouring word starts inside of current word
a.x > x
&& a.x <= x + word.upperCase.length
)
) {
test = false; // yes -> discard
}
}
// vertical word begins directly below?
if (a.orientation == "↓" && a.y - 1 == y) {
if (a.x >= x
&& a.x <= x + word.upperCase.length
) {
test = false; // yes -> discard
}
}
});
}
}
if (word.orientation == "↓") {
if (grid[y + i][x]
&& grid[y + i][x] != word.upperCase.substr(i, 1)
) {
test = false;
}
// neighbouring field empty?
if (!noEmptySpaces) {
/* check all used words if they run
* parallel in a neighbouring column
* alongside the current word */
q.each(used, function (a) {
if (a.orientation == "↓"
&& (a.x == x + 1
|| a.x == x - 1)
) {
if (// neighbouring word longer?
(a.y <= y &&
a.upperCase.y + a.upperCase.length >= y + word.upperCase.length)
||
// neighbouring word overlaps beginning of current word
(a.y <= y
&& a.y + a.upperCase.length >= y)
||
// neighbouring word starts inside of current word
(a.y > y
&& a.y <= y + word.upperCase.length)
) {
test = false; // leider nein
}
}
// horizontal word begins directly below?
if (a.orientation == "→"
&& (
a.x == x + 1
|| a.x == x - word.upperCase.length - 1
)
) {
if (a.y >= y
&& a.y <= y + word.upperCase.length
) {
test = false; // yes -> discard
}
}
});
}
}
if (word.orientation == "↘") {
if (grid[y + i][x + i]
&& grid[y + i][x + i] != word.upperCase.substr(i, 1)
) {
test = false;
}
}
if (word.orientation == "↗") {
if (grid[y - i][x + i]
&& grid[y - i][x + i] != word.upperCase.substr(i, 1)
) {
test = false;
}
}
}
// check fields before and after the word
if (word.orientation == "→") {
if (grid[y][x - 1]
|| grid[y][x + word.upperCase.length]
) {
test = false;
}
}
if (word.orientation == "↓") {
if (grid[y - 1][x]
|| grid[y + word.upperCase.length][x]
) {
test = false;
}
}
if (word.orientation == "↘") {
if (grid[y - 1][x - 1]
|| grid[y + word.upperCase.length][x + word.upperCase.length]
) {
test = false;
}
}
if (word.orientation == "↗") {
if (grid[y + 1][x - 1]
|| grid[y - word.upperCase.length][x + word.upperCase.length]
) {
test = false;
}
}
if (test) {
// word fits! -> no discarding
word.x = x;
word.y = y;
failed = []; // try failed words again
break; // no more tests for current word
}
}
if (test) {
break; // no more tests for current word
}
}
}
}
}
}
if (word.x < 0 && word.y < 0) {
/* no place in the grid to fit this word
* -> remember for later */
failed.push(word);
if (failed.length == (words.length - used.length)) {
/* there seems to be no suitable place in
* the grid for the remaining words...
* -> look for a free space for the current
* word!
* -> top (0), left (1), bottom(2) or right(3)
*/
taints ++;
r = Math.floor(Math.random() * 4);
if (r & 1 == 1) {
// left / right
y = Math.floor(grid.length / 2) - Math.floor(word.upperCase.length / 2);
// value must be greater or smaller
x = (r & 2) == 2 ? 0 : grid[0].length;
} else {
// top / bottom
x = Math.floor(grid[0].length / 2);
y = (r & 2) == 2 ? 0 : grid.length;
}
word.orientation = (r & 1) == 0 ? "→" : "↓";
/* check coordinates of used words
* in order to find suitable place */
q.each(used, function (a) {
if ((r & 1) == 1) {
// reduce left / right
if ((r & 2) == 0 && a.x < x) {
x = a.x;
}
if ((r & 2) == 2) {
test = a.x;
if (a.orientation == "→") {
test += a.upperCase.length;
}
if (test > x) {
x = test;
}
}
} else {
// reduce top / down
if ((r & 2) == 0 && a.y < y) {
y = a.y;
}
if ((r & 2) == 2) {
test = a.y;
if (a.orientation == "↓") {
test += a.upperCase.length;
}
if (test > y) {
y = test;
}
}
}
});
// suitable place found!
word.x = (r & 2) == 0 ? x - 2 : x + 2;
word.y = (r & 2) == 0 ? y - 2 : y + 2;
failed = [];
}
}
} else {
// first word -> immediately fit vertically
word.x = Math.floor(grid[0].length / 2);
word.y = Math.floor(grid.length / 2) - Math.floor(word.upperCase.length / 2);
word.orientation = "↓";
}
// save the word if it could successfully fit in the grid
if (word && word.x >= 0 && word.y >= 0) {
used.push(word);
// insert the word's letters into the grid
for (i = 0; i < word.upperCase.length; i++) {
if (word.orientation == "→") {
grid[word.y][word.x + i] = word.upperCase.substr(i, 1);
}
if (word.orientation == "↓") {
grid[word.y + i][word.x] = word.upperCase.substr(i, 1);
}
if (word.orientation == "↘") {
grid[word.y + i][word.x + i] = word.upperCase.substr(i, 1);
}
if (word.orientation == "↗") {
grid[word.y - i][word.x + i] = word.upperCase.substr(i, 1);
}
}
}
}
// save finished grid
finished.push({
grid: grid,
data: used,
taints: taints
});
}
// we might have more than one usable grid
if (taints > 0) {
/* there seems to be no perfect grid
* -> choose best among tainted ones */
test = false;
q.each(finished, function (f) {
if (!test || f.taints < test.taints) {
test = f;
}
});
// choose best try
grid = test.grid;
used = test.data;
}
// trim selected grid
a = {
x : {
min: grid[0].length,
max: 0
},
y : {
min: grid.length,
max: 0
}
};
for (y = 0; y < grid.length; y++) {
for (x = 0; x < grid[0].length; x++) {
// field occupied? -> use coordinates
if (grid[y][x]) {
// decrease min value if possible
a.x.min = (a.x.min > x) ? x : a.x.min;
a.y.min = (a.y.min > y) ? y : a.y.min;
// increase max value if needed
a.x.max = (a.x.max < x) ? x : a.x.max;
a.y.max = (a.y.max < y) ? y : a.y.max;
}
}
}
/* min/max values determined
* -> transfer grid contents into reduced grid */
test = new Array(a.y.max - a.y.min + 1);
// rows
for (y = 0; y < (a.y.max - a.y.min + 1); y++) {
test[y] = new Array(a.x.max - a.x.min + 1);
// columns
for (x = 0; x < (a.x.max - a.x.min + 1); x++) {
if (grid[y + a.y.min][x + a.x.min]) {
// copy contents
test[y][x] = grid[y + a.y.min][x + a.x.min];
}
}
}
grid = test; // replace old grid with reduced grid
// update coordinates of words inside reduced grid
for (i = 0; i < used.length; i++) {
used[i].x = used[i].x - a.x.min;
used[i].y = used[i].y - a.y.min;
}
return { grid: grid, words: used };
};
/**
* function to initiate a drag&drop operation
*
* This function returns
* - false if a drag&drop operation is in process
* - true if no drag&drop operation is in process
*
* @param Event
* @return bool
*/
t.dragStart = function (e) {
var test = t.getEventElement(e);
if (e.touches && e.touches[0]) {
t.lastCoords.left = e.touches[0].clientX;
t.lastCoords.top = e.touches[0].clientY;
}
/* make sure we got an element that really is supposed to
* be dragged around */
while (
test != document.body
&& (
!test.className
|| !test.classList.contains(t.draggableClass)
)
) {
test = test.parentNode;
}
if (test != document.body
&& test.classList.contains(t.draggableClass)
) {
t.dragMode = true;
// remove focus on any <input> elements
q.each(document.querySelectorAll("input"), function (i) {
try { i.blur(); }
catch (e) { }
});
t.dragElm = test;
t.dragElm.classList.add(t.draggingClass);
e.preventDefault();
}
return !t.dragMode;
};
/**
* function to end a drag&drop operation
*
* This function returns
* - false if a drag&drop operation is in process
* - true if no drag&drop operation is in process
*
* @param Event
* @return bool false
*/
t.dragStop = function (e) {
// no drag&drop element? end right here
if (!t.dragElm || !t.dragElm.className) {
return false;
}
t.repairDragAndDropOnIE();
if (t.dragging) {
// reset position of dragged element
t.dragElm.style.top = "";
t.dragElm.style.left = "";
t.dragElm.classList.remove(t.draggingClass);
}
// remove dragging state from dragged element
t.dragElm.classList.remove(t.draggingClass);
// resolve drag&drop operation?
if (t.dragging && t.resolveDragDrop) {
t.resolveDragDrop();
}
// empty drag&drop-related variables
t.dragElm = null;
t.dragging = false;
t.dragMode = false;
// un-highlight drag&drop target
if (t.highlightElm) {
t.highlightElm.classList.remove(t.highlightClass);
t.highlightElm = null;
}
return false;
};
/**
* function to record current mouse or touch coordinates
* and move dragged element during a drag&drop operation
*
* @param Event
* @return bool true
*/
t.dragWhile = function (e) {
var left = e.clientX,
top = e.clientY,
dx, dy;
if (e.touches) {
left = e.touches[0].clientX;
top = e.touches[0].clientY;
}
dx = t.lastCoords.left - left;
dy = t.lastCoords.top - top;
// save current coordinates
t.lastCoords.left = left;
t.lastCoords.top = top;
// no drag&drop operation in process? finish right here
if (!t.dragElm || !t.dragMode) {
return true;
}
// tell quiz that a drag&drop operation is in process
t.dragging = true;
e.preventDefault();
// calculate distance to last coordinates of the dragged element
left = 0; // presume the element hasn't been moved yet
top = 0;
// get actual relative position of the dragged element
if (t.dragElm.style.left) {
left = parseFloat(t.dragElm.style.left);
top = parseFloat(t.dragElm.style.top);
}
t.dragElm.style.left = left - dx + "px";
t.dragElm.style.top = top - dy + "px";
t.highlightTarget();
return true;
};
/**
* function to finalize a quiz
*
* This method expects a quiz to already have an element
* in the container which is stored as the quiz's t.pool
* property.
*
* - set a link blocker on any link
* - add quiz to framework's allQuizzes property
* - associate pieces with quiz
* - set result box
* - start the quiz
*
* @param bool don't call t.start()
*/
t.finalize = function (noStart) {
/* In a MediaWiki environment an <img> element is
* always embedded in a hyperlink which links to a
* page with meta data on this image. In an active
* quiz this link is irritating since it makes the
* browser leave the current quiz page. This is why
* a hyperlink blocker is necessary as long as a
* quiz is unfinished. */
var block = function (el) {
q.each(
el.getElementsByTagName("a"),
function (a) {
a.addEventListener("click", function (e) {
// allow only if quiz is finished
if (t.container.classList.contains(
t.finishedClass
)) {
return true;
}
e.preventDefault();
e.stopPropagation();
return false;
});
}
);
};
// set button text
t.resultButton.innerHTML = q.i18n[t.lang].check;
t.restartButton.innerHTML = q.i18n[t.lang].restart;
// equip restart button with functionality
t.restartButton.addEventListener("click", function () {
t.start();
});
// equip result button with functionality
t.resultButton.addEventListener("click", function () {
t.end();
});
// check container for hyperlinks
block(t.container);
/* Check drag&drop pieces since they have not been
* appended to the container yet */
q.each(t.pieces, block);
// add quiz to framework
q.allQuizzes.push(t);
// associate all pieces with this quiz
q.each(t.pieces, function (p) {
p.setAttribute("data-quiz", t.quizName);
});
// place result box after the pool element
t.pool.parentNode.insertBefore(
t.result,
t.pool.nextSibling
);
// place restart button into result box
t.result.appendChild(t.restartButton);
// set suitable ID for the quiz's container element
t.container.id = t.quizName;
// add eventlisteners to record mouse or touch coordinates
t.container.addEventListener("mousemove", t.dragWhile);
t.container.addEventListener("touchmove", t.dragWhile);
// equip required class names
t.pool.classList.add(t.poolClass);
t.result.classList.add(t.resultClass);
// let's start this quiz
if (!noStart) {
t.start();
}
};
/**
* function to determine the element that is to be dragged
* respecting touch devices as well
*
* @param Event
* @return HTMLElement
*/
t.getEventElement = function (e) {
e = e || window.event; // W3C DOM <-> IE
// touch device?
if (e.touches) {
return document.elementFromPoint(
e.touches[0].clientX,
e.touches[0].clientY
);
}
// mouse event
return e.target || e.srcElement; // W3C DOM <-> IE
};
/**
* function to hide or show the result button
*
* Showing the button means to append it to t.pool.
*
* @param bool
*/
t.hideResultButton = function (showInstead) {
if (!showInstead) {
if (t.resultButton.parentNode) {
t.resultButton.parentNode.removeChild(t.resultButton);
}
} else {
t.pool.appendChild(t.resultButton);
}
};
/**
* function to highlight a possible drag&drop target
*
* @param Event
* @return bool true
*/
t.highlightTarget = function () {
var targets;
// presume there is no current drag&drop target
t.highlightElm = null;
// we need the quiz's targets of course
targets = t.targets;
// the quiz's pool is a valid target, too!
targets.push(t.pool);
// iterate through all targets
q.each(
targets,
function (element){
var rect = element.getBoundingClientRect();
// un-highlight possible previous target
element.classList.remove(t.highlightClass);
// are we inside a possible drag&drop target?
if (rect.top < t.lastCoords.top
&& rect.left < t.lastCoords.left
&& rect.bottom > t.lastCoords.top
&& rect.right > t.lastCoords.left
) {
// yes!
element.classList.add(t.highlightClass);
t.highlightElm = element;
}
}
);
};
/**
* function to handle key strokes
*
* @param event
* @return bool false
*/
t.keyUp = function (e) {
var input = t.getEventElement(e),
fields = input.parentNode.getElementsByTagName("label"),
currentIndex = fields.length,
char;
q.each(fields, function (l, i) {
if (l.classList.contains(t.focusClass)) {
currentIndex = i;
}
});
switch (e.keyCode) {
case 27: // ESC
// close dialog
t.check();
break;
case 35: // End
// focus last field
fields[currentIndex].classList.remove(t.focusClass);
fields[fields.length -1].classList.add(t.focusClass);
break;
case 36: // Home
// focus first field
fields[currentIndex].classList.remove(t.focusClass);
fields[0].classList.add(t.focusClass);
break;
case 37: // Cursor left
case 8: // Backspace
// focus previous field
if (currentIndex < fields.length) {
fields[currentIndex].classList.remove(t.focusClass);
}
currentIndex = (
currentIndex > 0
? currentIndex -1
: 0
);
// empty <input> or its contents will get used!
input.value = "";
// remove field contents on back space
if (e.keyCode == 8) {
fields[currentIndex].innerHTML = String.fromCharCode(160);
}
fields[currentIndex].classList.add(t.focusClass);
break;
case 39: // Cursor right
// focus next field
fields[currentIndex].classList.remove(t.focusClass);
currentIndex = (
currentIndex === fields.length -1
? currentIndex
: currentIndex +1
);
fields[currentIndex].classList.add(t.focusClass);
break;
}
if (input.value.length > 0) {
// use first entered letter
char = q.utf8NormalizeToUpper(input.value.substr(0, 1));
// empty <input> (some Android softkeyboards need a timeout)
setTimeout(function () { input.value = ""; }, 10);
q.each(char, function (c) {
fields[currentIndex].innerHTML = c;
fields[currentIndex].classList.remove(t.focusClass);
if (currentIndex < fields.length -1) {
currentIndex++;
fields[currentIndex].classList.add(t.focusClass);
}
});
}
return false;
};
/**
* function to prepare the resultbox with feedback
*
* This method uses a quiz's attempts property to compute a
* standardized feedback to the user. A custom feedback can
* be used with an optional parameter.
*
* @param String optional
*/
t.prepareResult = function () {
var s = "";
// use standard feedback?
if (!arguments.length) {
// first the praise
s = (
t.attempts > 2
? q.i18n[t.lang].praise3
: q.i18n[t.lang]["praise" + t.attempts]
) + " ";
// then the result
s += (
t.attempts > 2
? q.i18n[t.lang].result3
: q.i18n[t.lang]["result" + t.attempts]
).replace("%n", t.attempts);
} else {
// custom feedback
s = arguments[0];
}
s += " ";
// empty box
while (t.result.firstChild) {
t.result.removeChild(t.result.firstChild);
}
// insert feedback and restart button
t.result.appendChild(document.createTextNode(s));
t.result.appendChild(t.restartButton);
};
/**
* function to remove event listeners from a quiz container
*/
t.removeDragDropListeners = function () {
t.container.removeEventListener("mousedown", t.dragStart);
t.container.removeEventListener("mouseup", t.dragStop);
t.container.removeEventListener("touchstart", t.dragStart);
t.container.removeEventListener("touchend", t.dragStop);
t.container.removeEventListener("touchcancel", t.dragStop);
};
/**
* function to supress unwanted text selection when drag&drop
* operation is in process
*
* @param bool on/off
*/
t.repairDragAndDropOnIE = function (on) {
var f = function () { return false; };
if (on) {
document.addEventListener("selectstart", f);
document.addEventListener("dragstart", f);
} else {
document.removeEventListener("selectstart", f);
document.removeEventListener("dragstart", f);
}
};
/**
* function to add event listener functions to a quiz container
*/
t.setDragDropListeners = function () {
t.container.addEventListener("mousedown", t.dragStart);
t.container.addEventListener("mouseup", t.dragStop);
t.container.addEventListener("touchstart", t.dragStart);
t.container.addEventListener("touchend", t.dragStop);
t.container.addEventListener("touchcancel", t.dragStop);
};
/**
* function to upload data about tries and quiz type
*
* This function expects a certain object window.rQuizUploader
* provided by an external JavaScript. If such an object is
* available and has the required structure, a quiz can use it
* to send a POST request to the server.
*/
t.uploadData = function () {
if (typeof window.rQuizUploader != "function") {
return;
}
window.rQuizUploader({
quizType: t.type,
attempts: t.attempts,
end: function (lastNoOfAttempts) {
var txt = "";
if (lastNoOfAttempts.length > 0) {
txt = (
q.i18n[t.lang].attemptsLastTime + " "
).replace(/%n/, lastNoOfAttempts);
}
t.restartButton.parentNode.insertBefore(
document.createTextNode(txt),
t.restartButton
);
t.result.classList.remove(t.waitClass);
}
});
t.result.classList.add(t.waitClass);
};
/* update this quiz's language setting
* if a lang attribute has been provided */
if (element.hasAttribute("lang")
&& q.i18n[element.getAttribute("lang")]
) {
t.lang = element.getAttribute("lang");
}
};
/**
* crossword quiz class
*
* This class inherits drag&drop functionality from the
* abstract quiz class.
*
* @param Object HTMLElement
*/
q.quizConstructors.crossword = function (element) {
var t = this;
// ensure inheritance
q.quizConstructors._abstractQuiz.call(t, element);
/*=================*
* quiz properties *
*=================*/
/**
* dialog for the user to input a word's letters
*
* @var Object HTMLElement
*/
t.dialog = document.createElement("dialog");
/**
* class name for focussed dialog fields
*
* @param String
*/
t.focusClass = "rquiz-focus";
/**
* form element for use in the quiz's dialog
*
* @var Object HTMLElement
*/
t.forms = {
h: document.createElement("form"),
v: document.createElement("form")
};
/**
* the visible grid
*
* @param Array
*/
t.grid = [];
/**
* class name for hidden result button
*
* @param String
*/
t.hiddenClass = "rquiz-hidden";
/**
* test input element for use in the quiz's form
*
* @var Object HTMLElement
*/
t.inputs = {
h: document.createElement("input"),
v: document.createElement("input")
};
/**
* printable contents
*
* @param String
*/
t.printSheet = document.createElement("table");
/**
* class name for element to unhide
*
* @param String
*/
t.showClass = "rquiz-show";
/**
* class name for the number tag in the dialog
*
* @param String
*/
t.tagClass = "rquiz-tag";
/**
* the words inside the grid and their positions
*
* @param Array
*/
t.words = [];
/*==============*
* quiz methods *
*==============*/
/**
* function to check if all fields are filled out and
* if the result button is to be displayed
*/
t.check = function () {
var complete = true, // expect a full quiz
c, x, y;
// check all cells
for (y = 0; y < t.grid.length; y++) {
for (x = 0; x < t.grid[y].length; x++) {
c = t.table.rows[y].cells[x];
if (c.hasAttribute("data-c")
&& q.trim(c.textContent).length < 1
) {
complete = false;
}
}
}
// offer result button?
t.hideResultButton(complete);
// close dialog
if (location.href.match(/\#rquiz\d+_dialog/)) {
history.back();
}
return false;
};
/**
* function to decide what to do when the user clicks something
*
* @param Event
* @return bool false
*/
t.click = function (e) {
var el = t.getEventElement(e),
instructions = document.querySelector(
"#%s dialog > div > p".replace(/%s/g, t.quizName)
),
closers = document.querySelectorAll(
(
// <dialog> itself (back drop)
"#%s dialog, "
// and the close icon
+ "#%s dialog > div > span"
).replace(/%s/g, t.quizName)
),
data = {
tag: 0,
words: []
},
div = t.dialog.getElementsByTagName("div")[0],
fields = t.dialog.getElementsByTagName("label");
// only do anything when the quiz isn't finished
if (t.container.classList.contains(t.finishedClass)) {
return false;
}
// toggle visibility of dialog instructions
if (el == instructions) {
instructions.classList.toggle(t.showClass);
}
// set focus on a dialog field?
if (el.tagName.match(/^label$/i)) {
q.each(fields, function (f) {
if (el == f) {
f.classList.add(t.focusClass);
} else {
f.classList.remove(t.focusClass);
}
});
}
// close dialog?
q.each(closers, function (c) {
if (el == c) {
t.check();
}
});
// show dialog?
if (el.hasAttribute("data-tag")) {
data.tag = el.getAttribute("data-tag");
// get solution words that start at this cell
q.each(t.words, function (word) {
if (t.table.rows[word.y]
&& t.table.rows[word.y].cells[word.x]
&& el == t.table.rows[word.y].cells[word.x]
) {
data.words.push(word);
}
});
}
// prepare dialog elements based on grid cell data
if (data.words.length && data.tag > 0) {
// remove forms from div
q.each(t.forms, function (f) {
if (f.parentNode) {
f.parentNode.removeChild(f);
}
});
// insert stuff for every word
q.each(data.words, function (word) {
var f = (
word.orientation == "→"
? t.forms.h
: t.forms.v
),
id = t.quizName + (
word.orientation == "→"
? "_ih"
: "_iv"
),
i, p, x, y;
// insert form
div.appendChild(f);
// empty form
while (f.firstChild) {
f.removeChild(f.firstChild);
}
// insert a hint for the solution
p = f.appendChild(document.createElement("p"));
// corresponding tag
p.appendChild(q.create({
tagName: "span",
text: data.tag,
className: t.tagClass
}));
p.appendChild(document.createTextNode(
word.orientation + " " + word.hint
));
// display fields for every letter of the word
p = f.appendChild(document.createElement("p"));
// add the corresponding input
p.appendChild(
word.orientation == "→"
? t.inputs.h
: t.inputs.v
);
for (i = 0; i < word.upperCase.length; i++) {
x = word.x;
y = word.y;
if (word.orientation == "→") {
x += i;
} else {
y += i;
}
p.appendChild(q.create({
tagName: "label",
htmlFor: id,
text: q.trim(
t.table.rows[y].cells[x].textContent
)
}));
// set coordinates of corresponding grid element
p.lastChild.setAttribute("data-x", x);
p.lastChild.setAttribute("data-y", y);
}
// add submit button
p.appendChild(q.create({
tagName: "button",
text: "↲"
}));
});
// show dialog
document.location.href = (
document.location.href.replace(/\#.*/, "")
+ "#" + t.quizName + "_dialog"
);
// calculate <div>'s position within <dialog>
setTimeout(
function () {
var height = div.offsetHeight;
div.style.marginTop = (
height < t.table.offsetHeight
? (t.table.offsetHeight - height) / 2 + "px"
: "0px"
);
},
100
);
// highlight first field and focus input
t.updateForm(t.dialog.getElementsByTagName("form")[0]);
}
return false;
};
/**
* function to end a quiz and show result
*/
t.end = function () {
var ok = true, c, x, y;
t.attempts++;
// check all cells
q.each(t.grid, function (row, y) {
q.each(row, function (cell, x) {
c = t.table.rows[y].cells[x];
if (cell && q.trim(c.textContent) != cell) {
ok = false;
}
});
});
if (ok) {
t.prepareResult();
t.container.classList.add(t.finishedClass);
t.uploadData();
}
};
/**
* function to handle dialog entries
*
* @param event
* @return bool false
*/
t.formSubmit = function (e) {
var el = t.getEventElement(e),
fields = el.getElementsByTagName("label"),
forms = t.dialog.getElementsByTagName("form");
// take each field's contents and write them into the grid
q.each(fields, function (f) {
var x = f.getAttribute("data-x"),
y = f.getAttribute("data-y");
t.table.rows[y].cells[x].innerHTML = f.innerHTML;
});
// remove current form from the dialog
el.parentNode.removeChild(el);
// update another form's fields?
if (forms.length) {
t.updateForm(forms[0]);
} else {
// no, close dialog
t.check();
}
e.preventDefault();
e.stopPropagation();
return false;
};
/**
* function to hide or show the result button
*
* Showing the button means to append it to t.pool.
*
* @param bool
*/
t.hideResultButton = function (showInstead) {
if (!showInstead) {
t.resultButton.classList.add(t.hiddenClass);
} else {
t.resultButton.classList.remove(t.hiddenClass);
}
};
/**
* start function
*
* This function prepares the quiz to be solved by the
* user. Since a quiz must be reset after a successful
* solution this function can restart the quiz, too.
*/
t.start = function () {
var counter = 1,
// true = interlinked words:
grid = t.createWordGrid(t.data, true),
// we only want the <tbody>:
printSheet = t.printSheet.getElementsByTagName("tbody")[0],
x, y, tr, td,
tryAgain; // this function might fail unexpectedly
t.grid = grid.grid;
t.words = grid.words;
// empty table
while (t.table.firstChild) {
t.table.removeChild(t.table.firstChild);
}
// prepare table for letters
for (y = 0; y < t.grid.length; y++) {
// add new row
tr = t.table.insertRow(
t.table.rows.length <= 0
? 0
: t.table.rows.length
);
for (x = 0; x < t.grid[0].length; x++) {
// add new cell
td = tr.appendChild(document.createElement("td"));
// any contents for this cell?
if (t.grid[y]
&& t.grid[y][x]
&& t.grid[y][x].length > 0
) {
td.setAttribute("data-c", t.grid[y][x]);
}
// fill cell with two
td.innerHTML = "aa".replace(
/a/g,
String.fromCharCode(160)
);
}
}
// add number tags to cells where a word begins
q.each(t.words, function (d) {
var o = "";
x = d.x;
y = d.y;
if (t.table.rows[y] && t.table.rows[y].cells[x]) {
td = t.table.rows[y].cells[x];
// add a number tag?
if (!td.hasAttribute("data-tag")) {
td.setAttribute("data-tag", counter++);
}
}
});
// remove finished marker
t.container.classList.remove(t.finishedClass);
t.check();
// fill print sheet with printable contents
q.each(printSheet.getElementsByTagName("td"), function (td) {
// empty first
td.parentNode.removeChild(td);
});
// create lists
q.each({"horizontal": "→", "vertical": "↓"}, function (o, s) {
var dl = document.createElement("dl"),
list = [],
tr = t.printSheet.getElementsByTagName("tr"),
td = document.createElement("td");
// use last table row to enter cells
tr[tr.length -1].appendChild(td);
// enter definition list into cell
td.appendChild(dl);
// create list with hints
q.each(t.words, function (w) {
if (w.orientation == o) {
// this seems to die occasionally
try {
list.push({
tag: t.table.rows[w.y].cells[w.x].getAttribute("data-tag"),
hint: w.hint
});
}
catch (e) { tryAgain = true; }
}
});
// sort list of hints according to tag number
list.sort(function (a, b) {
return a.tag - b.tag;
});
// fill definition list
q.each(list, function (l) {
// tag
dl.appendChild(q.create({
tagName: "dt",
text: l.tag
}));
// hint
dl.appendChild(q.create({
tagName: "dd",
text: l.hint
}));
});
});
if (tryAgain) {
t.start();
} else {
// position dialog over the grid table
setTimeout(
function () {
t.dialog.style.left = (t.table.offsetLeft -1) + "px";
t.dialog.style.top = (t.table.offsetTop -1) + "px";
t.dialog.style.height = (t.table.offsetHeight +2) + "px";
t.dialog.style.width = (t.table.parentNode.offsetWidth) + "px";
},
1000
);
t.pool.appendChild(t.resultButton);
}
};
/**
* update a dialog's form
*
* @param Object <form>
*/
t.updateForm = function (form) {
var fields = form.getElementsByTagName("label");
q.each(fields, function (f) {
var x = f.getAttribute("data-x"),
y = f.getAttribute("data-y");
f.innerHTML = q.trim(
t.table.rows[y].cells[x].textContent
);
});
// highlight first field and focus <input>
setTimeout(
function () {
fields[0].classList.add(t.focusClass);
form.getElementsByTagName("input")[0].focus();
},
300
);
};
/**
* setup
*
* This function prepares all the interactive elements
* later needed when the quiz is to (re-)start.
*/
(function () {
var f = function () {
try {
document.styleSheets[0].insertRule(
"#" + t.quizName + "_dialog:target { display: block; }",
document.styleSheets[0].cssRules.length
);
}
catch (e) {
/* a CSP mit prohibit "unsafe-inline" so this
* fallback might fail... */
document.querySelector("head").appendChild(
q.create({
tagName: "style",
text: "#" + t.quizName + "_dialog:target { display: block; }"
})
);
}
},
i, j, tab;
// extract quiz data from a <table> element in the container
tab = t.container.getElementsByTagName("table");
// <table> found?
if (!tab[0]) {
return false;
}
// get quiz data
q.each(tab[0].getElementsByTagName("tr"), function (tr) {
var data = [];
// search row for values
q.each(tr.getElementsByTagName("td"), function (td) {
var v = q.trim(
td.innerHTML.replace(/ /, " ")
);
if (v.length) {
data.push(v);
}
});
/* Accept data only if it has a first and a
* second element (search term and hint)! */
if (data.length > 1) {
t.data.push({
hint: data[1],
upperCase: q.utf8NormalizeToUpper(
data[0]
)
});
}
});
// initialize quiz if suitable data has been found
if (!t.data.length) {
return;
}
// use initial table as grid display
t.table = tab[0];
// place pool directly after the grid display
tab[0].parentNode.insertBefore(t.pool, tab[0].nextSibling);
// insert the dialog after the table
tab[0].parentNode.insertBefore(t.dialog, tab[0].nextSibling);
/* insert wrapper div into dialog since there is no
* sufficient support for CSS backdrop yet */
t.dialog.appendChild(document.createElement("div"));
// equip dialog with id so CSS can display it based on :target
t.dialog.id = t.quizName + "_dialog";
// equip input fields with IDs and filling guidelines
q.each(t.inputs, function (node, key) {
node.id = t.quizName + "_i" + key;
node.setAttribute("autocomplete", "off");
/* If input loses focus the highlighted fields
* must lose highlighting, too. */
node.addEventListener("blur", function (e) {
q.each(node.form.getElementsByTagName("label"), function (l) {
l.classList.remove(t.focusClass);
});
});
});
// equip document with a new stylesheet rule to display the dialog
try { f(); }
catch (e) { q.functionsAfterCssReady.push(f); }
// equip dialog with closing button
t.dialog.firstChild.appendChild(q.create({
tagName: "span",
text: "×"
}));
// equip dialog with header
t.dialog.firstChild.appendChild(q.create({
tagName: "h2",
text: q.i18n[t.lang].input
}));
// equip dialog with instructions
t.dialog.firstChild.appendChild(q.create({
tagName: "p",
text: q.i18n[t.lang].enterNotice
}));
// equip <form>s with a submit blocker
q.each(t.forms, function (f) {
f.addEventListener("submit", t.formSubmit);
});
// equip container with a listener on clicks
t.container.addEventListener("click", t.click);
// equip <input>s with a listener on keyup
q.each(t.inputs, function (i) {
i.addEventListener("keyup", t.keyUp);
});
// equip print sheet with suitable class name and header
t.printSheet.appendChild(document.createElement("tbody"));
t.printSheet.className = t.printOnlyClass;
t.printSheet.lastChild.appendChild(
document.createElement("tr")
);
// table headings
q.each({"horizontal": "→", "vertical": "↓"}, function (o, s) {
t.printSheet.lastChild.lastChild.appendChild(
q.create({
tagName: "th",
text: q.i18n[t.lang][s] + " " + o
})
);
});
t.printSheet.lastChild.appendChild(document.createElement("tr"));
// append print sheet to container
t.container.appendChild(t.printSheet);
// finalize and start quiz
t.finalize();
}());
};
/**
* gap-filling quiz class
*
* This class inherits drag&drop functionality from the
* abstract quiz class.
*
* @param Object HTMLElement
*/
q.quizConstructors.gapfill = function (element) {
var t = this;
// ensure inheritance
q.quizConstructors._abstractQuiz.call(t, element);
/*=================*
* quiz properties *
*=================*/
/**
* list of text inputs
*
* @var Array
*/
t.inputs = [];
/*==============*
* quiz methods *
*==============*/
/**
* function to end a quiz and show result
*/
t.end = function () {
var backToPool = [],
withErrors = false; // hope for a full success
// increase number of attempts
t.attempts++;
// check if all pieces are in their correct target
q.each(t.pieces, function (p) {
/* Compare the piece's data-match value with the
* one from it's parent node which must be a
* t.target element. */
var m = p.getAttribute("data-match");
if (p.parentNode.getAttribute("data-match") != m) {
// wrong! go back!
backToPool.push(p);
withErrors = true;
}
});
// check if all input fields have a correct value
q.each(t.inputs, function (i) {
var ok = false, // presume a wrong value
v = q.trim(i.element.value);
// we might have more than one valid solution
q.each(i.solutions, function (s) {
if (v == s) {
ok = true;
}
});
if (!ok) {
i.element.classList.add(t.errorClass);
withErrors = true;
}
});
if (backToPool.length) {
q.shuffleArray(backToPool);
// some pieces need to go back
q.each(backToPool, function (p) {
t.pool.appendChild(p);
});
t.hideResultButton();
}
/* If everything is correct, we can update the
* result box and finish this quiz! */
if (!withErrors) {
t.prepareResult();
// remove draggable abilty from all pieces
q.each(t.pieces, function (p) {
p.classList.remove(t.draggableClass);
});
// set all inputs to readonly so no tampering after grading
q.each(t.inputs, function (i) {
console.dir(i);
i.element.setAttribute("readonly", "");
});
t.removeDragDropListeners(t.container);
t.container.classList.add(t.finishedClass);
t.uploadData();
} else {
t.hideResultButton();
}
};
/**
* function to determine result of drag&drop operation
*/
t.resolveDragDrop = function () {
var noEmptyInputs = true,
elements;
/* We want to place t.dragElm into t.highlightElm
* if there is a t.highlightElm. Any present element
* gets thrown back into t.pool. */
if (t.highlightElm) {
while (t.highlightElm.firstChild
&& t.highlightElm != t.pool
) {
t.pool.appendChild(t.highlightElm.firstChild);
}
t.highlightElm.appendChild(t.dragElm);
}
// remove t.resultButton from t.pool
if (t.resultButton.parentNode == t.pool) {
t.pool.removeChild(t.resultButton);
}
/* Do we need to show the result button
* because t.pool is now empty and all input fields
* have been filled out? */
elements = t.pool.getElementsByClassName(
t.draggableClass
);
q.each(t.inputs, function (i) {
if (!i.element.value) {
noEmptyInputs = false;
}
});
if (!elements.length && noEmptyInputs) {
t.pool.appendChild(t.resultButton);
}
};
/**
* start function
*
* This function prepares the quiz to be solved by the
* user. Since a quiz must be reset after a successful
* solution this function can restart the quiz, too.
*/
t.start = function () {
var pool = [],
r;
q.shuffleArray(t.pieces);
/* Find all relevant "piece" elements and put
* them in the pool. Make them draggable by giving
* them the draggable class name. */
q.each(t.pieces, function (p) {
t.pool.appendChild(p);
p.classList.add(t.draggableClass);
});
// empty all <input>s and remove any "readonly" attribute
q.each(t.inputs, function (i) {
i.element.value = "";
i.element.removeAttribute("readonly");
});
t.setDragDropListeners();
t.container.classList.remove(t.finishedClass);
if (t.resultButton.parentNode) {
t.pool.removeChild(t.resultButton);
}
while (t.result.firstChild != t.restartButton) {
t.result.removeChild(t.result.firstChild);
}
t.attempts = 0;
};
/**
* setup
*
* This function prepares all the interactive elements
* later needed when the quiz is to (re-)start.
*/
(function () {
var counter = 0, // for incremental IDs
gaps = [];
/* we need all <b>, <em>, <i> and <strong>
* within our container */
q.each(
document.querySelectorAll(
".rquiz-gapfill b"
+ ", .rquiz-gapfill em"
+ ", .rquiz-gapfill i"
+ ", .rquiz-gapfill strong"
),
function (el) {
var p = el;
// determine if el is inside our container
while (p != document.body
&& p != t.container
) {
p = p.parentNode;
}
// el is inside our container
if (p == t.container) {
gaps.push(el);
}
}
);
// no gaps? end right here
if (!gaps.length) {
return;
}
// process gaps
q.each(gaps, function (el) {
var html = q.trim(el.innerHTML),
i, l, n, // nodes (HTMLElements)
solutions = [];
// input field or drag&drop element?
if (html.match(/\(/)) {
// input field: determine possible solutions
i = html.replace(
/[\t\r\n]/g, " "
).replace(
/^([^(]+).*$/, "$1"
).replace(
/( | )/, " "
).replace(
/ +/, " "
).split("|");
// trim every solution
q.each(i, function (s) {
s = q.trim(s);
// weed out empty solutions
if (s.length) {
solutions.push(s);
}
});
if (solutions.length) {
// we put the input field inside a <label>
l = q.create({
tagName: "label",
htmlFor: t.quizName + "_" + counter,
// put any possible hints inside <label>
innerHTML: " " + (
html
.replace(/^[^(]*(\(.*) *$/, "$1")
.replace(/ ?\(\)$/, "")
)
});
i = q.create({
tagName: "input",
id: t.quizName + "_" + counter
});
i.setAttribute(
"placeholder",
q.i18n[t.lang].tempText
);
/* We need to check if we can offer the
* result button whenever something gets
* typed into an <input>. */
i.addEventListener(
"keyup",
function () {
i.classList.remove(t.errorClass);
t.resolveDragDrop();
}
);
// put <input> at the beginning of <label>
l.insertBefore(i, l.firstChild);
// replace original element with <label>
el.parentNode.replaceChild(l, el);
t.inputs.push({
element: i,
solutions: solutions
});
t.data.push({
element: i,
solutions: solutions,
type: "input"
});
counter++;
}
} else {
// drag&drop element
html = q.trim(el.innerHTML);
// don't accept empty gaps
if (html.length) {
// create a drag&drop piece
i = q.create({
tagName: "span",
className: t.piecesClass
});
i.innerHTML = html;
i.setAttribute("data-match", counter);
// create a drag&drop target
l = q.create({
tagName: "span",
className: t.targetClass
});
l.setAttribute("data-match", counter);
counter++;
/* We might have another piece with
* identical contents. In this case we
* want to copy its data-match value and
* not the counter's. */
q.each(t.pieces, function (p) {
var v = "";
if (p.innerHTML == html) {
v = p.getAttribute("data-match");
}
if (v.length) {
// use found value
l.setAttribute("data-match", v);
i.setAttribute("data-match", v);
/* modify counter since we
* haven't used it */
counter--;
}
});
t.pieces.push(i);
t.targets.push(l);
/* puzzle piece? Puzzle pieces get an
* additional class name "puzzle" if
* they originate from an ancestor with
* this class name! */
n = el;
while (n != document.body
&& n != t.container
) {
n = n.parentNode;
if (n.classList.contains("puzzle")) {
i.classList.add("puzzle");
}
}
// replace original element with target
el.parentNode.replaceChild(l, el);
t.data.push({
o: {
piece: i,
target: l
},
type: "drag&drop"
});
}
}
});
// finalize and start quiz
if (t.data.length) {
t.container.appendChild(t.pool);
t.finalize();
}
}());
};
/**
* matching quiz class
*
* This class inherits drag&drop functionality from the
* abstract quiz class.
*
* @param Object HTMLElement
*/
q.quizConstructors.matching = function (element) {
var t = this;
// ensure inheritance
q.quizConstructors._abstractQuiz.call(t, element);
/*=================*
* quiz properties *
*=================*/
/**
* the quiz's game mode
*
* The quiz supports the matching of pairs or the
* matching of categories.
*
* "pairs": If the original table consisted of rows
* with exactly two columns then the quiz will randomly
* take one element of a pair and let the user match it
* with the other one.
*
* "categories": If the original table consisted of rows
* with mor than two columns then the quiz will take the
* first element of such a group and let the user match
* all the other elements with this one.
*
* @var String (pairs|categories)
*/
t.mode = "pairs";
/*==============*
* quiz methods *
*==============*/
/**
* function to end a quiz and show result
*/
t.end = function () {
var backToPool = [];
// increase number of attempts
t.attempts++;
// check if all pieces are in their correct group
q.each(t.targets, function (target) {
var group, pieces;
if (target != t.pool) {
pieces = target.getElementsByClassName(
t.piecesClass
);
group = pieces.item(0).getAttribute("data-group");
q.each(pieces, function (p) {
if (group !== p.getAttribute("data-group")) {
backToPool.push(p);
}
});
}
});
if (backToPool.length) {
q.shuffleArray(backToPool);
// some pieces need to go back
q.each(backToPool, function (p) {
t.pool.appendChild(p);
});
t.hideResultButton();
} else {
/* If everything is correct, we can update the
* result box and finish this quiz! */
t.prepareResult();
q.each(t.pieces, function (p) {
p.classList.remove(t.draggableClass);
});
t.removeDragDropListeners(t.container);
t.container.classList.add(t.finishedClass);
t.uploadData();
}
};
/**
* function to determine result of drag&drop operation
*/
t.resolveDragDrop = function () {
var elements;
/* We want to place t.dragElm into t.highlightElm.
* In "pairs" mode we need to move any previous
* element in t.highlightElm back into t.pool, but
* not the very first element! The very first
* element doesn't have the t.draggabelClass which
* should make things a little easier. */
t.highlightElm.appendChild(t.dragElm);
if (t.mode == "pairs") {
elements = t.highlightElm.getElementsByClassName(
t.draggableClass
);
// we must keep only the last draggable element
while (elements.length > 1) {
t.pool.appendChild(elements[0]);
}
}
t.hideResultButton(
t.pool.getElementsByClassName(t.draggableClass).length < 1
);
};
/**
* start function
*
* This function prepares the quiz to be solved by the
* user. Since a quiz must be reset after a successful
* solution this function can restart the quiz, too.
*/
t.start = function () {
var targets = [],
pool = [],
r; // random number
// we want to randomly choose a target
q.each(t.targets, function (n) {
if (n != t.pool) {
targets.push(n);
}
});
q.shuffleArray(targets);
q.each(t.data, function (group, g) {
// which element is to be in the target area?
r = (
t.mode == "pairs"
? r = Math.floor(Math.random() * 2)
: 0 // in category mode it's always the first
);
/* find all relevant "piece" elements and put
* them either in the pool or in a target
* element */
q.each(t.pieces, function (piece) {
if (g == piece.getAttribute("data-group")) {
// check for correct group item
if (r == piece.getAttribute("data-no")) {
// place into target area
targets[g].appendChild(piece);
} else {
// place into pool
pool.push(piece);
piece.classList.add(t.draggableClass);
}
}
});
// shuffle pieces and pour into t.pool element
q.shuffleArray(pool);
q.each(pool, function (p) {
t.pool.appendChild(p);
});
});
t.setDragDropListeners();
t.container.classList.remove(t.finishedClass);
t.hideResultButton();
while (t.result.firstChild != t.restartButton) {
t.result.removeChild(t.result.firstChild);
}
t.attempts = 0;
};
/**
* setup
*
* This function prepares all the interactive elements
* later needed when the quiz is to (re-)start.
*/
(function () {
var i, j, tab, piece;
/* extract quiz data from a <table> element
* in the container */
tab = t.container.getElementsByTagName("table");
// no <table> found? end right here
if (!tab[0]) {
return;
}
// get quiz data
q.each(tab[0].getElementsByTagName("tr"), function (tr) {
var group = [];
// search row for values
q.each(tr.getElementsByTagName("td"), function (td) {
var v = q.trim(
td.innerHTML.replace(/ /, " ")
);
if (v.length) {
group.push(v);
}
});
/* add group to data if it contains at least
* two elements */
if (group.length > 1) {
t.data.push(group);
}
/* change quiz mode to "categories"
* if a group has more than two elements */
if (group.length > 2) {
t.mode = "categories";
}
});
// initialize quiz if suitable data has been found
if (!t.data.length) {
return;
}
// place this quiz's pool directly before the initial <table>
tab[0].parentNode.insertBefore(t.pool, tab[0]);
// remove initial <table>
tab[0].parentNode.removeChild(tab[0]);
/* A <span> element will be created for every
* corresponding value from the quiz's data
* elements. It will carry the following
* data-attributes:
*
* data-group: an integer that defines to which
* group this value belongs
*
* data-no: the index under which this value is
* stored inside the group - needed to determine the
* first element as category name in "categories"
* quiz mode
*/
for (i = 0; i < t.data.length; i++) {
for (j = 0; j < t.data[i].length; j++) {
piece = q.create({
tagName: "span",
className: t.piecesClass,
innerHTML: t.data[i][j]
});
piece.setAttribute("data-group", i);
piece.setAttribute("data-no", j);
t.pieces.push(piece);
}
}
// create target elements for drag&drop
q.each(t.data, function () {
// remember these elements as "targets"
t.targets.push(
// place elements before the pool element
t.pool.parentNode.insertBefore(
q.create({
tagName: "p",
className: t.targetClass
}),
t.pool
)
);
});
// finalize and start quiz
t.finalize();
}());
};
/**
* memory quiz class
*
* This class inherits drag&drop functionality from the
* abstract quiz class.
*
* @param Object HTMLElement
*/
q.quizConstructors.memo = function (element) {
var t = this;
// ensure inheritance
q.quizConstructors._abstractQuiz.call(t, element);
/*=================*
* quiz properties *
*=================*/
/**
* class name for a turnable card
*
* @param String
*/
t.cardClass = "rquiz-memocard";
/**
* list of all turnable cards
*
* @param Array
*/
t.cards = [];
/**
* class name for a permanently upturned card
*
* @param String
*/
t.fixedClass = "rquiz-fixed";
/**
* class name for an upturned card
*
* @param String
*/
t.openClass = "rquiz-open";
/**
* number of elements in a set
*
* By default a memo quiz wants a user to find pairs.
* However this quiz also supports n-tuples to be found.
*
* @param int
*/
t.setLength = 2;
/**
* flag for waiting until cards are flipped back
*
* @param bool
*/
t.wait = false;
/*==============*
* quiz methods *
*==============*/
/**
* click listener function
*
* This function listens to the "click" event and makes
* the cards turn. It also detects if the restart button
* gets used.
*
* @param Event
* @return bool false
*/
t.click = function (e) {
var ok = true, // presume correct user choice
open = [],
el, p;
// We don't do anything as long as we have to wait!
if (!t.wait) {
el = t.getEventElement(e);
// any of the cards?
while (el != document.body
&& !el.classList.contains(t.cardClass)
) {
el = el.parentNode;
}
if (el.classList.contains(t.cardClass)) {
// is this card to be flipped over?
if (!el.classList.contains(t.fixedClass)
&& !el.classList.contains(t.openClass)
) {
// yes
el.classList.add(t.openClass);
// was this the last card of a possible set?
q.each(t.cards, function (c) {
if (c.classList.contains(t.openClass)) {
open.push(c);
}
});
// no? do nothing
if (open.length < t.setLength) {
return false;
}
// set complete: check if correct
q.each(open, function (c, i) {
if (open[0].getAttribute("data-group")
!= open[i].getAttribute("data-group")
) {
ok = false;
}
});
if (!ok) {
t.attempts++;
// block all interactions temporarily
t.wait = true;
// flip opened cards back after 2 seconds
setTimeout(
function () {
q.each(open, function (c) {
c.classList.remove(t.openClass);
});
// now the user may interact again
t.wait = false;
},
2000
);
} else {
// set correct! fix opened cards
q.each(open, function (c) {
c.classList.remove(t.openClass);
c.classList.add(t.fixedClass);
});
// are all cards now open?
q.each(t.cards, function (c) {
if (!c.classList.contains(
t.fixedClass
)) {
ok = false;
}
});
if (ok) {
setTimeout(
function () {
// quiz is finished!
t.container.classList.add(
t.finishedClass
);
t.uploadData();
// unfix all cards
q.each(t.cards, function (c) {
c.classList.remove(t.fixedClass);
});
// update result box
t.prepareResult();
},
1100
);
}
}
}
}
}
return false;
};
/**
* start function
*
* This function prepares the quiz to be solved by the
* user. Since a quiz must be reset after a successful
* solution this function can restart the quiz, too.
*/
t.start = function () {
// empty pool
while (t.pool.firstChild) {
t.pool.removeChild(t.pool.firstChild);
}
// shuffle the cards and put them in the pool
q.shuffleArray(t.cards);
q.each(t.cards, function (c) {
t.pool.appendChild(c);
});
t.container.classList.remove(t.finishedClass);
t.attempts = 0;
};
/**
* setup
*
* This function prepares all the interactive elements
* later needed when the quiz is to (re-)start.
*/
(function () {
var setLength = 1000,
/* This set length is insane, but how many
* elements per set are sensible anyways? And
* where should we set a maximum limit? */
i, j, card, tab;
// extract quiz data from a <table> element in the container
tab = t.container.getElementsByTagName("table");
// no <table> found? end right here
if (!tab[0]) {
return;
}
// get quiz data
q.each(tab[0].getElementsByTagName("tr"), function (tr) {
var group = [];
// search row for values
q.each(tr.getElementsByTagName("td"), function (td) {
var v = q.trim(
td.innerHTML.replace(/ /, " ")
);
if (v.length) {
group.push(v);
}
});
// we need at least pairs
if (group.length > 1) {
// update set length based on smallest group
if (group.length < setLength) {
setLength = group.length;
}
t.data.push(group);
}
});
// initialize quiz if suitable data has been found
if (!t.data.length) {
return;
}
// define actual number of elements in a set
t.setLength = setLength;
// place quiz's pool directly before initial <table>
tab[0].parentNode.insertBefore(t.pool, tab[0]);
// remove initial <table>
tab[0].parentNode.removeChild(tab[0]);
// supress unwanted visual behaviour
t.pool.classList.remove(t.poolClass);
/* A <span> element will be created for every
* corresponding value from the quiz's data
* elements. It will contain a "data-group"
* attribute carrying an integer referring to the
* group to where this value belongs. */
for (i = 0; i < t.data.length; i++) {
for (j = 0; j < setLength; j++) {
card = q.create({
tagName: "span",
className: t.cardClass
});
card.setAttribute("data-group", i);
t.cards.push(card);
/* In order to achieve a nice flip
* animation for our cards, we need a wrapper <span>
* and two <span>s in the wrapper <span> as front
* and back side. */
card.appendChild(
// wrapper
document.createElement("span")
);
card.firstChild.appendChild(
// front
document.createElement("span")
);
card.firstChild.appendChild(
// back
document.createElement("span")
);
/* In order to achieve vertical centering we
* need two put our card's contents inside
* a nested <span>. */
card.firstChild.lastChild.appendChild(q.create({
tagName: "span",
innerHTML: t.data[i][j]
}));
}
}
// prepare result box
t.container.appendChild(t.result);
// add a click listener on our entire container
t.container.addEventListener("click", t.click);
// finalize and start quiz
t.finalize();
}());
};
/**
* multiple choice quiz class
*
* This class inherits drag&drop functionality from the
* abstract quiz class.
*
* @param Object HTMLElement
*/
q.quizConstructors.multichoice = function (element) {
var t = this;
// ensure inheritance
q.quizConstructors._abstractQuiz.call(t, element);
/*==============*
* quiz methods *
*==============*/
/**
* function to end a quiz and show result
*/
t.end = function () {
var a = 0, // all answers
c = 0, // correct choices
f = q.i18n[t.lang].percentageResult;
// calculate success
q.each(t.data, function (d) {
q.each(d.ol.getElementsByTagName("input"), function (i) {
var correct = !i.parentNode.hasAttribute("data-x");
a++; // count this answer
// count answers if correctly (not) chosen
if ((i.checked && correct)
|| (!i.checked && !correct)
) {
c++;
}
// disable input so no tampering after grading
i.disabled = true;
});
});
// prepare feedback message
t.prepareResult(
f.replace("%n", Math.floor(10000 * c/a) / 100) + " "
);
t.container.classList.add(t.finishedClass);
t.uploadData();
};
/**
* start function
*
* This function prepares the quiz to be solved by the
* user. Since a quiz must be reset after a successful
* solution this function can restart the quiz, too.
*/
t.start = function () {
t.container.classList.remove(t.finishedClass);
// remove all questions from the container
q.each(t.data, function (d) {
// uncheck all answers and enable them
q.each(d.ol.getElementsByTagName("input"), function (i) {
i.checked = false;
i.disabled = false;
});
if (d.p.parentNode) {
d.p.parentNode.removeChild(d.p);
}
// and the list of possible answers
while (d.ol.firstChild) {
d.ol.removeChild(d.ol.firstChild);
}
if (d.ol.parentNode) {
d.ol.parentNode.removeChild(d.ol);
}
});
// mix questions
q.shuffleArray(t.data);
// place answers before the pool
q.each(t.data, function (d) {
t.container.insertBefore(d.p, t.pool);
// and the list of possible answers
t.container.insertBefore(d.ol, t.pool);
// mix answers before placing them into the quiz
q.shuffleArray(d.answers);
q.each(d.answers, function (li) {
d.ol.appendChild(li);
});
});
};
/**
* setup
*
* This function prepares all the interactive elements
* later needed when the quiz is to (re-)start.
*/
(function () {
var counter = 0; // incremental ID values
/* We expect paragraph elements that have at least
* two sets of parenthesis which will be possible
* answering options. Any contents outside these
* parentheticals will be ignored, except the
* contents before them since that presumably would
* be the question. */
q.each(
document.querySelectorAll(".rquiz-multichoice p"),
function (p) {
var el = p,
html = q.trim(
p.innerHTML.replace(
/[\t\r\n]/g,
" "
)
),
ok, // flag
d; // data element
// Is the element from our quiz?
while (el != document.body
&& el != t.container
) {
el = el.parentNode;
}
// Not from our quiz? End right here!
if (el != t.container) {
return;
}
// extract data
d = {
answers: [],
ol: document.createElement("ol"),
p: q.create({
tagName: "p",
text: q.trim(html.replace(/\(.*/, ""))
})
};
// get possible answers
ok = false; // presume there is no correct answer
html.replace(/\(([^)]+)\)/g, function (dummy, s) {
var i = q.create({
tagName: "input",
type: "checkbox"
}),
label = q.create({
tagName: "label",
text: " " + s.replace(/^!/, "")
}),
li = document.createElement("li"),
correct = !(s.match(/^!/));
li.appendChild(i);
li.appendChild(label);
if (correct) {
ok = true;
} else {
li.setAttribute("data-x", "");
}
d.answers.push(li);
});
/* accept data only if there is more than
* one answer and at least one among them
* marked as correct */
if (d.answers.length > 1 && ok) {
// set "id" and "for" attributes
q.each(d.answers, function (a) {
var id = t.quizName + "i" + counter++;
a.getElementsByTagName("input")[0].id = id;
a.getElementsByTagName("label")[0].htmlFor = id;
});
// store in quiz data
t.data.push(d);
// remove original paragraph
p.parentNode.removeChild(p);
}
}
);
// initialize quiz if suitable data has been found
if (t.data.length) {
// prepare pool with result button
t.container.appendChild(t.pool);
t.hideResultButton(true);
// finalize and start quiz
t.finalize();
}
}());
};
/**
* word guessing quiz class
*
* This class inherits drag&drop functionality from the
* abstract quiz class.
*
* @param Object HTMLElement
*/
q.quizConstructors.wordguess = function (element) {
var t = this;
// ensure class inheritance
q.quizConstructors._abstractQuiz.call(this, element);
/*=================*
* quiz properties *
*=================*/
/**
* counter image
*
* @param Object HTMLElement
*/
t.counter = document.createElement("p");
/**
* class name for the counter image
*
* @param String
*/
t.counterClass = "rquiz-counter";
/**
* current word index
*
* @param int
*/
t.current = 0;
/**
* list of correctly guessed letters
*
* @param Object HTMLElement
*/
t.guessedChars = document.createElement("ul");
/**
* list of incorrectly guessed letters
*
* @param Object HTMLElement
*/
t.guessedCharsIncorrect = document.createElement("ul");
/**
* list of correctly guessed words
*
* @param Object HTMLElement
*/
t.guessedWords = document.createElement("ol");
/**
* input element for character input
*
* @param Object HTMLElement
*/
t.input = document.createElement("input");
/**
* number of remaining tries
*
* @param int
*/
t.remaining = 10;
/**
* class name for element to unhide
*
* @param String
*/
t.showClass = "rquiz-show";
/*==============*
* quiz methods *
*==============*/
/**
* function to focus or unfocus input fields
*
* @param Event
* @return bool false
*/
t.click = function (e) {
var el = t.getEventElement(e),
help = t.pool.getElementsByTagName("p")[0];
// focus input
t.input.focus();
// toggle visibility of help instructions
if (el == help) {
help.classList.toggle(t.showClass);
} else {
help.classList.remove(t.showClass);
}
return false;
};
/**
* function to end a quiz and show result
*
* This function first checks if the quiz may go on and possibly
* leads to the next word-to-be-guessed.
*/
t.end = function () {
if (t.remaining > 0 && t.current < t.data.length) {
return t.next();
}
// show result box and finish this quiz
t.prepareResult("");
t.container.classList.add(t.finishedClass);
t.uploadData();
setTimeout(function () { t.restartButton.focus(); }, 100);
};
/**
* function to handle key strokes
*
* @param event
* @return bool false
*/
t.keyUp = function (e) {
var input = t.getEventElement(e),
// use first entered letter
char = q.utf8NormalizeToUpper(q.trim(input.value)).substr(0, 1),
correct = t.guessedChars.getElementsByTagName("li"),
incorrect = t.guessedCharsIncorrect.getElementsByTagName("li"),
ok;
if (char.length < 1) {
return false;
}
// check if we can use the input
q.each(correct, function (li) {
if (li.getAttribute("data-c") == char) {
li.innerHTML = char;
ok = true;
}
});
if (!ok) {
// find char in list of incorrectly guessed letters
q.each(incorrect, function (li) {
if (li.textContent == char) {
ok = true;
}
});
}
// need to add char to the list of incorrectly guessed letters?
if (!ok) {
t.guessedCharsIncorrect.appendChild(q.create({
tagName: "li",
text: char
}));
t.remaining--;
t.updateCounter();
}
// no more tries left?
if (t.remaining < 1) {
t.end();
}
// empty <input> (some Android softkeyboards need a timeout)
setTimeout(function () { input.value = ""; }, 10);
// completely guessed a word?
ok = true; // expect success
q.each(correct, function (li) {
if (li.textContent != li.getAttribute("data-c")) {
ok = false;
}
});
if (ok) {
// insert found word into display of correctly guessed words
q.each(t.guessedWords.getElementsByTagName("li"), function (li) {
if (li.textContent == "" && ok) {
li.innerHTML = t.data[t.current].original;
ok = false;
}
});
// bonus for user
if (t.remaining < 10) {
t.remaining++;
t.updateCounter();
}
// is this the end?
t.end();
}
return false;
};
/**
* function to offer the next word
*/
t.next = function () {
t.current++;
// already had the last word?
if (t.current == t.data.length) {
return t.end();
}
// empty lists of guessed letters
q.each([t.guessedChars, t.guessedCharsIncorrect], function (list) {
while (list.firstChild) {
list.removeChild(list.firstChild);
}
});
// fill list of correctly guessed letters with empty fields
q.each(t.data[t.current].upperCase.split(""), function (s) {
t.guessedChars.appendChild(
document.createElement("li")
);
// we need this later to identify a correctsolution
t.guessedChars.lastChild.setAttribute(
"data-c",
q.utf8NormalizeToUpper(q.trim(s))
);
});
};
/**
* start function
*
* This function prepares the quiz to be solved by the
* user. Since a quiz must be reset after a successful
* solution this function can restart the quiz, too.
*/
t.start = function () {
// empty list of correctly guessed words
while (t.guessedWords.firstChild) {
t.guessedWords.removeChild(t.guessedWords.firstChild);
}
// fill list of correctly guessed words with empty items
q.each(t.data, function (d) {
t.guessedWords.appendChild(document.createElement("li"));
});
// (re-)mix all words
q.shuffleArray(t.data);
// reset counter
t.remaining = 10;
t.updateCounter();
// show quiz (again)
t.container.classList.remove(t.finishedClass);
// reset current word index
t.current = -1; // gets set to 0 by t.next()
// get this quiz going
t.next();
};
/**
* function to update the counter's image and title attribute
*/
t.updateCounter = function () {
t.counter.setAttribute("data-remaining", t.remaining);
t.counter.setAttribute(
"title",
q.i18n[t.lang].remainingTries.replace("%n", t.remaining)
);
};
/**
* setup
*
* This function prepares all the interactive elements
* later needed when the quiz is to (re-)start.
*/
(function () {
var tab;
/* extract quiz data from a <table> element
* in the container */
tab = t.container.getElementsByTagName("table");
// no <table> found? end right here
if (!tab[0]) {
return;
}
// get quiz data
q.each(tab[0].getElementsByTagName("td"), function (td) {
var txt = q.trim(td.textContent);
// add to data if it contains real text
if (txt.length > 0) {
t.data.push({
original: txt,
upperCase: q.utf8NormalizeToUpper(txt)
});
}
});
// initialize quiz if suitable data has been found
if (!t.data.length) {
return;
}
// this time our pool needs to be a div
t.pool = q.create({
tagName: "div",
className: t.poolClass
});
// replace the initial <table> with pool
tab[0].parentNode.replaceChild(t.pool, tab[0]);
// fill pool with stuff
t.pool.appendChild(q.create({
tagName: "p",
text: q.i18n[t.lang].enterNoticeWordGuessing
}));
t.pool.appendChild(t.counter);
t.counter.classList.add(t.counterClass);
/* add a section with
* - input element
* - two lists
* 1: correctly guessed letters
* 2: incorrectly guessed letters
*/
t.pool.appendChild(document.createElement("section"));
t.pool.lastChild.appendChild(t.input);
t.pool.lastChild.appendChild(t.guessedChars);
t.pool.lastChild.appendChild(t.guessedCharsIncorrect);
// We need an <h2> and <ol> so we wrap it in an <aside>
t.pool.appendChild(document.createElement("aside"));
t.pool.lastChild.appendChild(q.create({
tagName: "h2",
text: q.i18n[t.lang].foundWords
}));
t.pool.lastChild.appendChild(t.guessedWords);
// equip container with a listener on clicks
t.container.addEventListener("click", t.click);
// equip <input> with a listener on keyup and an ID
t.input.addEventListener("keyup", t.keyUp);
t.input.id = t.quizName + "_input";
// finalize and start quiz
t.finalize(true); // true = don't call t.start()
// show restart button
t.container.classList.add(t.finishedClass);
}());
};
/**
* word jumbling quiz class
*
* This class inherits drag&drop functionality from the
* abstract quiz class.
*
* @param Object HTMLElement
*/
q.quizConstructors.wordjumble = function (element) {
var t = this;
// ensure inheritance
q.quizConstructors._abstractQuiz.call(t, element);
/*=================*
* quiz properties *
*=================*/
/**
* list of text inputs
*
* @var Array
*/
t.inputs = [];
/*==============*
* quiz methods *
*==============*/
/**
* function to check if all <input>s are filled out and
* if the result button is to be displayed
*/
t.check = function () {
var complete = true; // presume that no <input> is empty
// check all inputs
q.each(t.data, function (d) {
if (!d.input.value.length) {
complete = false;
}
});
// If everything is complete, we can finish this quiz!
t.hideResultButton(complete);
};
/**
* function to end the quiz and show result
*/
t.end = function () {
var complete = true, // presume that all
ok = true; // hope for a full success
// increase number of attempts
t.attempts++;
// check all inputs
q.each(t.data, function (d) {
var original = d.input.getAttribute("data-solution"),
s = original.toLowerCase(),
v = q.trim(d.input.value.toLowerCase());
/* We accept input if its transformation to
* lower-case equals the original's
* transformation to lower-case. */
if (s != v) {
ok = false;
d.input.classList.add(t.errorClass);
}
});
/* If everything is correct, we can update the
* result box and finish this quiz! */
if (ok) {
// replace input values with original solution
q.each(t.data, function (d) {
d.input.value = d.input.getAttribute("data-solution");
d.input.setAttribute("readonly", "");
});
t.prepareResult();
t.container.classList.add(t.finishedClass);
t.uploadData();
} else {
t.hideResultButton();
}
};
/**
* start function
*
* This function prepares the quiz to be solved by the
* user. Since a quiz must be reset after a successful
* solution this function can restart the quiz, too.
*/
t.start = function () {
// prepare the <label>s with new hints
q.each(t.data, function (d) {
var s = d.input.getAttribute("data-solution"),
jumble = s.toLowerCase().split("");
// freshly mixed hint
q.shuffleArray(jumble);
// rebuild contents for <label>
while (d.label.firstChild) {
d.label.removeChild(d.label.firstChild);
}
d.label.appendChild(d.input);
// erase any previous contents and blocks
d.input.value = "";
d.input.removeAttribute("readonly", "");
d.label.appendChild(document.createTextNode(
" (" + jumble.join("") + ")"
));
});
t.container.classList.remove(t.finishedClass);
t.hideResultButton();
t.attempts = 0;
};
/**
* setup
*
* This function prepares all the interactive elements
* later needed when the quiz is to (re-)start.
*/
(function () {
var gaps = [];
/* we need all <b>, <em>, <i> and <strong>
* within our container */
q.each(
document.querySelectorAll(
".rquiz-wordjumble b"
+ ", .rquiz-wordjumble em"
+ ", .rquiz-wordjumble i"
+ ", .rquiz-wordjumble strong"
),
function (el) {
var p = el;
// determine if el is inside our container
while (p != document.body
&& p != t.container
) {
p = p.parentNode;
}
// el is inside our container
if (p == t.container) {
gaps.push(el);
}
}
);
// found no gaps? end right here
if (!gaps.length) {
return;
}
// process gaps
q.each(gaps, function (el) {
var s = q.trim(el.textContent),
i = document.createElement("input"),
l = document.createElement("label");
// remember correct solution
i.setAttribute("data-solution", s);
/* We need to check if we can offer the
* result button whenever something gets
* typed into an <input>. */
i.addEventListener(
"keyup",
function () {
i.classList.remove(t.errorClass);
t.check();
}
);
// replace original element with <label>
el.parentNode.replaceChild(l, el);
t.data.push({
input: i,
label: l
});
});
// finalize and start quiz
if (t.data.length) {
t.container.appendChild(t.pool);
t.finalize();
}
}());
};
/**
* word searching quiz class
*
* This class inherits drag&drop functionality from the
* abstract quiz class.
*
* @param Object HTMLElement
*/
q.quizConstructors.wordsearch = function (element) {
var t = this;
// ensure inheritance
q.quizConstructors._abstractQuiz.call(t, element);
/*=================*
* quiz properties *
*=================*/
/**
* class name for a letter of a successfully found word
*
* @var String
*/
t.fixedClass = "rquiz-fixed";
/**
* display of the words a user has already found
*
* @param Object HTMLElement
*/
t.found = document.createElement("ol");
/**
* element at which a highlighting ends
*
* @param Object HTMLElement
*/
t.highlightEnd = null;
/**
* element at which a highlighting begins
*
* @param Object HTMLElement
*/
t.highlightStart = null;
/*==============*
* quiz methods *
*==============*/
/**
* function to initiate a drag&drop operation
*
* This function returns
* - false if a drag&drop operation is in process
* - true if no drag&drop operation is in process
*
* @param Event
* @return bool
*/
t.dragStart = function (e) {
var el = t.getEventElement(e);
if (el.tagName && el.tagName.match(/^td$/i)) {
// remember in which cell the dragging started
t.highlightStart = el;
t.dragMode = true;
// suppress regular text selection
if (window.getSelection) {
window.getSelection().removeAllRanges();
}
el.classList.add(t.highlightClass);
e.preventDefault();
e.stopPropagation();
return false;
}
return true;
};
/**
* function to end a drag&drop operation
*
* This function returns
* - false if a drag&drop operation is in process
* - true if no drag&drop operation is in process
*
* @param Event
* @return bool false
*/
t.dragStop = function (e) {
// anything found?
t.resolveDragDrop();
t.removeHighlight();
t.dragMode = false;
// allow regular text selection in IE
t.repairDragAndDropOnIE();
return true;
};
/**
* function to record current mouse or touch coordinates
* and move dragged element during a drag&drop operation
*
* @param Event
* @return bool
*/
t.dragWhile = function (e) {
var el = t.getEventElement(e);
if (t.dragMode) {
t.suppressTextSelection();
if (el.tagName && el.tagName.match(/^td$/i)) {
t.highlightEnd = el;
// remove existing highlighting
t.removeHighlight();
// set new highlighting
t.highlight();
}
e.preventDefault();
e.stopPropagation();
}
return true;
};
/**
* function to end a quiz and show result
*/
t.end = function () {
var ok = true, // expect all words to have been found
c, x, y;
t.attempts++;
// check all list items
q.each(t.found.getElementsByTagName("li"), function (li) {
if (q.trim(li.textContent).length < 1) {
ok = false;
}
});
if (ok) {
// prepare a custom feedback
c = "";
if (t.attempts - t.words.length > 1) {
c += q.i18n[t.lang].praise3
} else {
c += q.i18n[t.lang][
"praise" + (1 + t.attempts - t.words.length)
]
}
c += " " + q.i18n[t.lang].result3.replace(/%n/, t.attempts);
t.prepareResult(c);
t.container.classList.add(t.finishedClass);
t.removeDragDropListeners(t.container);
t.uploadData();
}
};
/**
* function to highlight adjacent cells either horizontally,
* vertically or diagonally
*/
t.highlight = function () {
var el, end, inclination, orientation, start, x, y;
// determine coordinates of start and end of highlight
for (y = 0; y < t.table.rows.length; y++) {
for (x = 0; x < t.table.rows[y].cells.length; x++) {
if (t.table.rows[y].cells[x] == t.highlightStart) {
start = { x: x, y: y };
}
if (t.table.rows[y].cells[x] == t.highlightEnd) {
end = { x: x, y: y };
}
}
}
// any error?
if (!start || !end) {
return false;
}
// determine inclination
if ((end.y - start.y) === 0) {
// Division By Zero!
inclination = 2; // a value above 1.5 is sufficient
} else {
// quotient is "legal"
inclination = (end.x - start.x) / (end.y - start.y);
}
// determine orientation from inclination and coordinates
if (Math.abs(inclination) >= 0.5
&& Math.abs(inclination) <= 1.5
) {
// diagonal
orientation = (
inclination > 0
? "↘"
: "↙"
);
} else {
// horizontal/vertical
orientation = (
Math.abs(inclination) > 1
? "→"
: "↓"
);
}
// highlight all suitable cells
x = start.x;
y = start.y;
el = t.table.rows[y].cells[x];
while (el) {
el.classList.add(t.highlightClass);
switch (orientation) {
case "↓":
// only modify y
if (start.y > end.y && y > end.y) {
y--;
}
if (start.y < end.y && y < end.y) {
y++;
}
break;
case "→":
// only modify x
if (start.x > end.x && x > end.x) {
x--;
}
if (start.x < end.x && x < end.x) {
x++;
}
break;
case "↘":
if (start.x > end.x && x > end.x
&&
start.y > end.y && y > end.y
) {
x--;
y--;
}
if (start.x < end.x && x < end.x
&&
start.y < end.y && y < end.y
) {
x++;
y++;
}
break;
case "↙":
if (start.x > end.x && x > end.x
&&
start.y < end.y && y < end.y
) {
x--;
y++;
}
if (start.x < end.x && x < end.x
&&
start.y > end.y && y > end.y
) {
x++;
y--;
}
break;
}
// stop if there is no more cell to highlight
el = (
el != t.table.rows[y].cells[x]
? t.table.rows[y].cells[x]
: false
);
}
};
/**
* function to determine result of drag&drop operation
*/
t.resolveDragDrop = function () {
var highlighted = t.table.getElementsByClassName(
t.highlightClass
);
if (highlighted.length < 1) {
return;
}
// check every word if its letters have all been highlighted
q.each(t.words, function (word) {
var found = 0, // highlighted elements at correct coordinates
c = 0, // current letter
x = word.x,
y = word.y;
// has current letter been highlighted?
while (found === c && c < word.upperCase.length) {
q.each(highlighted, function (el) {
if (t.table.rows[y].cells[x]
&& el == t.table.rows[y].cells[x]
) {
found++;
}
});
// next letter
c++;
// next position
switch (word.orientation) {
case "↓":
y++;
break;
case "→":
x++;
break;
case "↘":
x++;
y++;
break;
case "↗":
x++;
y--;
break;
}
}
if (found === c && highlighted.length == c) {
// fix highlighted cells
q.each(highlighted, function(el) {
el.classList.add(t.fixedClass);
});
// add found word onto the display of found words
found = false; // expect it is not yet in the list
q.each(t.found.getElementsByTagName("li"), function (li) {
if (q.trim(li.textContent) == word.original) {
found = true;
}
});
if (!found) {
// add to list
q.each(t.found.getElementsByTagName("li"), function (li) {
if (q.trim(li.textContent).length < 1
&& !found
) {
li.appendChild(q.create({
tagName: "strong",
text: word.original
}));
found = true;
}
});
}
}
});
t.removeHighlight();
t.highlightEnd = null;
t.highlightStart = null;
t.dragMode = false;
// try if quiz is finished
t.end();
};
/**
* function to remove any highlight markers
*/
t.removeHighlight = function () {
q.each(t.table.getElementsByTagName("td"), function (td) {
td.classList.remove(t.highlightClass);
});
};
/**
* start function
*
* This function prepares the quiz to be solved by the
* user. Since a quiz must be reset after a successful
* solution this function can restart the quiz, too.
*/
t.start = function () {
var counter = 1,
// true = interlinked words:
grid = t.createWordGrid(t.data, true, true),
x, y, tr, td;
t.attempts = 0;
t.grid = grid.grid;
t.words = grid.words;
// empty table
while (t.table.firstChild) {
t.table.removeChild(t.table.firstChild);
}
// prepare table for letters
for (y = 0; y < t.grid.length; y++) {
// add new row
tr = t.table.insertRow(y);
for (x = 0; x < t.grid[0].length; x++) {
// add new cell
tr.appendChild(q.create({
tagName: "td",
text: (
// do we have defined contents?
t.grid[y][x]
// yes
? t.grid[y][x]
// no -> generate random character
: String.fromCharCode(
65 + Math.floor(Math.random() * 26)
).toUpperCase()
)
}));
}
}
// prepare list of already found words
while (t.found.firstChild) {
t.found.removeChild(t.found.firstChild);
}
q.each(t.words, function (word) {
t.found.appendChild(document.createElement("li"));
});
t.setDragDropListeners();
t.container.classList.remove(t.finishedClass);
};
/**
* function to suppress text selection
*/
t.suppressTextSelection = function () {
if (window.getSelection) {
window.getSelection().removeAllRanges();
}
t.repairDragAndDropOnIE(true);
};
/**
* setup
*
* This function prepares all the interactive elements
* later needed when the quiz is to (re-)start.
*/
(function () {
var tab, aside;
// extract quiz data from a <table> element in the container
tab = t.container.getElementsByTagName("table");
// <table> found?
if (!tab[0]) {
return false;
}
// get quiz data
q.each(tab[0].getElementsByTagName("td"), function (td) {
var v;
// trim cell contents
v = q.trim(
td.innerHTML.replace(/ /, " ")
);
if (v.length) {
t.data.push({
original: v,
upperCase: q.utf8NormalizeToUpper(v)
});
}
});
// initialize quiz if suitable data has been found
if (!t.data.length) {
return;
}
// use initial table as grid display
t.table = tab[0];
// place display of found words directly after the grid display
aside = tab[0].parentNode.insertBefore(
document.createElement("aside"),
tab[0].nextSibling
);
aside.appendChild(q.create({
tagName: "h2",
text: q.i18n[t.lang].foundWords
}));
aside.appendChild(t.found);
// place pool directly after the display of found words
tab[0].parentNode.insertBefore(t.pool, aside.nextSibling);
// finalize and start quiz
t.finalize();
}());
};
// start the whole thing
q.begin();
// expose an init function to the window object for a script loader
window.rQuizInit = function () {
q.begin();
};
}());