app/assets/javascripts/hitme.js
// returns a unique ID to be used to identify an element within the
// webpage. Depends on the current state of the page, i.e. if we already
// are in ask-skipped-questions-again mode.
function getUniqId(q, a) {
var qid = (typeof q === 'object' ? q.id : q);
if(qid === undefined || qid === null) {
throwWithAlert('invalid question id', "question=" + q + " qid=" + qid);
}
var id = "blockQuest" + qid;
if(!window.currentHitme.nagAboutSkippedQuestions) id += 'repeat';
if(a) {
id += "_answer" + (typeof a === 'object' ? a.id : a);
}
return id;
}
function isAnswerSelectionCorrect(answers) {
var correct = true;
$.each(answers, function(ind, answ) {
answ = $(answ);
// answer correct, but not checked
if(answ.data('correct') === 1 && !answ.hasClass("active")) correct = false;
// answer wrong, but checked
if(answ.data('correct') === 0 && answ.hasClass("active")) correct = false;
if(!correct) return false; // i.e. break
});
return correct;
}
function checkForQuestionPreview() {
var singleQuestion = getHash("question");
if(singleQuestion === undefined || !singleQuestion) return;
var h = new H.Hitme();
window.currentHitme = h;
h.setupSingleQuestionMode();
}
function validateNumberOnly(self) {
self = $(self);
var val = parseInt(self.val().replace(/[^0-9]/g, "") || 10);
var max = parseInt(self.attr('max') || 999999);
var min = parseInt(self.attr('min') ||-999999);
self.val(Math.max(min, Math.min(max, val)));
}
function parseMatrix(orig) {
var s = $.trim(orig);
var rows = s.split(/[\r\n]+/);
rows = $.map(rows, function(r) {
return $.trim(r).split(/\s+/).join(" ");
});
return rows.join(" ");
}
function showNextHint(elm) {
var hidden = $(elm).siblings(":hidden");
hidden.first().animate(CONST.showAnimation);
if(hidden.length === 1) $(elm).animate(CONST.hideAnimation);
}
function renderStarred(question) {
c = "";
c += '<div class="star">';
if(window.loggedIn) {
c += question.starred ? '★ ' : '';
c += '<a onclick="handleStarredClick(this)" '
c += 'data-id="'+question.id+'" ';
c += 'data-starred="'+(question.starred ? 1 : 0)+'">Frage ';
c += question.starred ? 'gemerkt' : 'merken';
c += '</a>';
c += ' ';
}
var xx = Routes.perma_question_path(question.id);
c += '⚓ <a href="'+xx+'" target="_blank">Link zur Frage</a>';
c += '</div>';
return c;
}
function handleStarredClick(link) {
var l = $(link);
var s = l.data('starred');
var id = l.data('id');
var r = s ? Routes.unstar_question_path(id) : Routes.star_question_path(id);
$.ajax({
url: r,
}).done(function(data) {
var fakeQ = {id: id, starred: data};
l.parent().replaceWith(renderStarred(fakeQ));
}).fail(function() {
l.text('irgendwas ist kaputt…').addClass('disable');
});
}
function getQuestionById(id) {
var quest = null;
$.each(window.currentHitme.questions, function(ind, q) {
if(q.id === id) {
quest = q;
return false;
}
});
return quest;
}
function ensureValidDifficultySelection() {
var any = false;
$('input[type=checkbox][name^=difficulty]').each(function() {
if($(this).is('checked')) {
any = true;
return false;
}
});
if(!any) $('input[type=checkbox][name^=difficulty]:first').prop('checked', true);
}
function disableOptions() {
$('#options input').attr("disabled", "disabled");
$('#options').animate(CONST.hideAnimation);
}
function enableOptions() {
$('#options input').removeAttr("disabled");
}
function hideCategories() {
$('h3 + .toggle:visible').each(function(i, cat) {
$(cat).prev().click();
});
var s = $('#categories a.button, #start-button');
disableLinks(s);
}
function getDifficulties() {
var diff = [];
$('input[type=checkbox][name^=difficulty]').each(function() {
if(this.checked) {
diff.push($(this).val());
}
});
return diff.join("_");
}
function showAllCategories() {
$('#categories .toggle').animate(CONST.showAnimation);
var s = $('a.disable, #start-button');
enableLinks(s);
}
function animateVisibilityHiddenShow(elms) {
elms.css('visibility','visible').hide().fadeIn('slow');
}
function animateDisplayNoneShow(elms) {
elms.css('display','inline');
}
function disableLinks(selector) {
$(selector).each(function(ind, s) {
s = $(s);
s.addClass('disable')
.data('oldonclick', s.attr('onclick'))
.attr('onclick', '');
});
}
function enableLinks(selector) {
$(selector).each(function(ind, s) {
s = $(s);
s.removeClass('disable').attr('onclick', s.data('oldonclick'));
});
}
function getURLForRootQuestions(categoryIds) {
var diff = this.getDifficulties();
var count = $('#quantity').val();
var studyPath = $('#study_path').val();
var h = {categories: categoryIds.join("_"), count: count, difficulty: diff, study_path: studyPath}
return Routes.main_question_path(h);
}
function getRootQuestions(categoryIds, context, successCallback) {
// example: http://0.0.0.0:3000/main/questions?categories=6_8&count=10&difficulty=10_20_30&study_path=3
$.ajax({
url: getURLForRootQuestions(categoryIds),
}).done(function(data) {
successCallback(context, data);
}).fail(function() {
alert("Die Anfrage konnte leider nicht bearbeitet werden. Möglicherweise hat der Server ein Problem.");
});
}
function getSingleQuestion(questionId, context, successCallback) {
$.ajax({
url: Routes.main_single_question_path({id: questionId}),
}).done(function(data) {
successCallback(context, data);
}).fail(function() {
alert("Die Anfrage konnte leider nicht bearbeitet werden. Möglicherweise hat der Server ein Problem.");
});
}
function getMultipleQuestion(questionIds, context, successCallback) {
$.ajax({
url: Routes.main_multiple_question_path({id: questionIds}),
}).done(function(data) {
successCallback(context, data);
}).fail(function() {
alert("Die Anfrage konnte leider nicht bearbeitet werden. Möglicherweise hat der Server ein Problem.");
});
}
function answersGivenCount() {
var a = window.H.answersGiven;
return a.fail.length + a.correct.length + a.skip.length;
}
// sees if there’s a subquestion for the current answer. If there is,
// and the user has activated subquestions it will be inserted next.
// Returns true if a subquestion has been inserted.
function maybeInsertSubquestion(aid) {
if(!$('#subquestions').is(':checked')) {
// user doesn’t want subquestions, skip
return false;
}
var q = window.currentQuestion;
var s;
$.each(q.answers, function(ind, answ) {
if(answ.id === parseInt(aid)) {
s = answ.subquestion;
return false;
}
});
// this answer doesn’t have a subquestion
if(!s) return false;
var c = window.currentHitme;
var p = c.questPositionPointer;
c.questions.splice(p+1, 0, s);
return true;
}
H = {};
window.H = H;
window.currentHitme = null;
Array.prototype.diff = function(a) {
return this.filter(function(i) {return !(a.indexOf(i) > -1);});
};
window.CONST = {
hideAnimation: {opacity: 'hide', height: 'hide', marginTop: 'hide',
marginBottom: 'hide', paddingTop: 'hide', paddingBottom: 'hide'},
showAnimation: {opacity: 'show', height: 'show', marginTop: 'show',
marginBottom: 'show', paddingTop: 'show', paddingBottom: 'show'},
stayAtBottom: { duration: 'slow',
step: function(now, fx) {
var absTop = $(this).offset().top;
var relTop = absTop - $(window).scrollTop();
// do not scroll past the top of the element.
// 50 pixels leave some space for the border
if(relTop < 50) return;
$('html, body').scrollTop(99999999);
}
}
}
// constructor
H.Hitme = function() {
// no selection? GONNA SELECT 'EM ALL!
if($('.inline-chooser .active').length === 0)
$('.inline-chooser .toggleable').addClass('active');
this.cats = $('.inline-chooser .active').map(function(i, m) { return $(m).data("id"); }).get();
this._this = this;
disableOptions();
ensureValidDifficultySelection();
hideCategories();
};
// members
H.Hitme.prototype = {
setupCategoryQuestionMode: function() {
getRootQuestions(this.cats, this, this.rootQuestionsAvailable);
this.questPositionPointer = -1;
this.nagAboutSkippedQuestions = true;
this.skippedQuestionsData = [];
this.answersGiven = {correct: [], fail: [], skip: []};
},
setupSingleQuestionMode: function() {
var question = getHash("question");
var ids = question.split(",");
var length = ids.length;
$("#quantity").val(length);
if (length == 1) {
getSingleQuestion(question, this, this.rootQuestionsAvailable);
} else {
getMultipleQuestion(question, this, this.rootQuestionsAvailable);
}
this.questPositionPointer = -1;
this.nagAboutSkippedQuestions = false;
this.skippedQuestionsData = [];
this.answersGiven = {correct: [], fail: [], skip: []};
},
giveMore: function() {
$('.hideMeOnMore').remove();
H.hitme(this.cat);
},
rootQuestionsAvailable: function(_this, data) {
_this.questions = data;
_this.showNext();
},
_renderAnswersForQuestion: function(quest) {
var s = "";
$.each(shuffle(quest.answers), function(ind, a) {
s += '<div>'
s += '<a class="button toggleable" id="'+getUniqId(quest, a)+'"';
s += ' onclick="$(this).toggleClass(\'active\');"';
s += ' data-correct="'+a.correct+'" data-qid="'+quest.id+'"';
s += ' data-aid="'+a.id+'"><div class="tex">'+a.html+'</div></a>';
s += '<span>'+answer_correctness[a.correct]+'</span>';
s += '<span>(das hattest Du angekreuzt)</span>';
s += '</div>';
});
return s;
},
_handleQuestionSubmit: function() {
// gather details
//~ var question = getQuestionById($(this).data('qid'));
var quest = window.currentQuestion;
var answerChooser = $(this).parent().siblings(".answer-chooser, .answer-chooser-matrix").first();
var action = $(this).data('action');
var boxSelector = '#' + getUniqId(window.currentQuestion);
var s = window.stat_reporting;
// disable ui
$(this).parent().children("a").addClass("disable");
answerChooser.find("a").addClass("disable").removeAttr("onclick");
answerChooser.find("textarea").attr('disabled', 'disabled');
// handle action
if(action === 'skip') {
window.currentHitme.skippedQuestionsData.push(quest);
window.currentHitme.answersGiven.skip.push(boxSelector);
$(this).parent().append("<span>Du hast diese Frage übersprungen</span>");
s.skipped();
} else if(action === 'save') {
$(boxSelector).addClass('reveal');
var correct;
if(quest.matrix) {
var m = parseMatrix(answerChooser.find("textarea").val());
var solution = window.currentQuestion.matrix_solution;
correct = solution.toLowerCase() === m.toLowerCase();
s.set_answers([m]);
} else {
var answ = answerChooser.find("a");
correct = isAnswerSelectionCorrect(answ);
// insert subquestions for selected answers if any
var selAnsw = answerChooser.find("a.active");
$.each(selAnsw, function(ind, answ) {
maybeInsertSubquestion($(answ).data('aid'));
});
// show which questions were selected by the user
$.each(selAnsw, function(ind, answ) {
animateVisibilityHiddenShow($(answ).siblings().last());
});
var selAnswIds = $.map(selAnsw, function(answ, ind) {
return $(answ).data('aid');
});
s.set_answers(selAnswIds);
}
if(correct) {
//~ console.log("q" + quest.id + " answered correctly");
window.currentHitme.answersGiven.correct.push(boxSelector);
s.success();
} else {
//~ console.log("q" + quest.id + " answered incorrectly");
window.currentHitme.answersGiven.fail.push(boxSelector);
s.failed();
}
} else {
throwWithAlert('Unsupported action. This is a coding error.');
}
window.currentHitme.showNext();
},
_showNextQuestion: function() {
this.questPositionPointer++;
var q = window.currentQuestion = this.questions[this.questPositionPointer];
var code = '<div style="display:none" id="'+getUniqId(q.id)+'" class="box hideMeOnMore">'; // outer box
code += '<h3 class="count">Frage</h3>';
if (q.video) { // inner box
code += '<div style="float:left;width: 70%;" >';
} else {
code += '<div>';
}
code += '<div class="tex">'+q.html+'</div>'
code += '<br/>';
if(q.hints && q.hints.length >= 1) {
code += '<div>'
$.each(q.hints, function(ind, hint) {
code += '<div style="display: none;margin: 5px 0">'+hint+'</div>';
});
code += '<a onclick="showNextHint(this);">Hinweis anzeigen</a>';
code += '</div>';
}
code += renderStarred(q);
var cls;
code += '<div class="answer-chooser'+(q.matrix?"-matrix":"")+'">'
if(q.matrix) {
var a = q.answers[0];
code += 'Trage unten die Lösung ein. Matrizen schreibst Du einfach mittels Leerzeichen und Zeilenumbrüchen. Die Anzahl der Leerzeichen ist dabei egal.<br/><br/>';
code += '<div style="float:left;width: 45%; overflow-y: show; overflow-x: hidden;">';
code += '<label for="'+getUniqId(q, a)+'">Deine Lösung</label><br class="clear"/>';
code += '<textarea id="'+getUniqId(q, a)+'" class="matrixmode"></textarea>';
code += '<div class="tex previewer" id="'+getUniqId(q, a)+'previewer"></div>';
code += '<br/>';
code += '</div>';
code += '<div style="float:right;width: 45%" class="initiallyHidden">';
code += '<strong>Unsere Lösung</strong><br class="clear"/>';
code += '<div class="tex">'+a.html+'</div></div>';
code += '<br class="clear"/>';
} else {
code += this._renderAnswersForQuestion(q);
}
code += '</div>'; // answer-chooser
code += '<br/><div class="answer-submit button-group">';
code += '<a class="button big" data-qid="'+q.id+'" title="Günther Jauch: Sind Sie sich wirklich sicher?" data-action="save">Antwort übernehmen</a>';
code += '<a class="button big" data-qid="'+q.id+'" data-action="skip">Frage überspringen</a>';
code += '</div>';
code += '</div>'; // inner box
if (q.video) {
code += '<div style="float:right;width: 30%" class="displayHidden">' + q.video + '</div><br class="clear"/>';
}
code += '</div>'; // outer box
$(code).appendTo('body');
$('.answer-submit:last').one('click', 'a', this._handleQuestionSubmit);
// render math first, then expand
var render = function() {
if(window.animationDisabled) {
var elm = $("#"+getUniqId(q.id));
elm.show();
$("html, body").scrollTop(elm.offset().top);
} else {
$("#"+getUniqId(q.id)).animate(CONST.showAnimation, CONST.stayAtBottom);
}
}
MathJax.Hub.Queue(["Typeset",MathJax.Hub, render]);
// add preview for matrix questions
if(q.matrix) {
window.matrixModePreview = null;
var textarea = $('#'+getUniqId(q, a));
var previewer = $('#'+getUniqId(q, a)+'previewer');
textarea.keyup(function() {
if(window.matrixModePreview) clearTimeout(window.matrixModePreview);
window.matrixModePreview = setTimeout(function() {
var v = parseMatrix(textarea.val());
if(v === "") return previewer.html("");
v = shortMatrixStrToTeX(v);
previewer.html(v);
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
}, 100);
});
}
if($('#comiccheckbox').is(':checked')) {
setTimeout("XkcdLoader.preload()", 100);
}
// start recording
window.stat_reporting = new StatReporting().record(q.id);
},
_reshowSkippedQuestions: function() {
var w = window.currentHitme;
w.answersGiven.skip = [];
w.questions = $.extend(true, [], w.skippedQuestionsData); // deep copy
w.questPositionPointer = -1;
w.showNext();
},
_showSkippedQuestionsNagDialog: function() {
// show only once
if(!this.nagAboutSkippedQuestions) return false;
this.nagAboutSkippedQuestions = false;
if(this.answersGiven.skip.length === 0) return false;
var code = '<div style="display:none;" class="box hideMeOnMore reshowskipped">'
+ '<h3>Übersprungene Fragen</h3>'
+ '<p>Du hast ' + this.answersGiven.skip.length + ' Frage(n) übersprungen. Sollen sie nochmal angezeigt werden, oder möchtest Du abschließen?</p>'
+ '<div class="button-group">'
+ '<a onclick="window.currentHitme._showFinishDialog(); disableLinks(\'.reshowskipped a\');" class="button big">Block abschließen</a>'
+ '<a onclick="window.currentHitme._reshowSkippedQuestions(); disableLinks(\'.reshowskipped a\');" class="button big">nochmal vorlegen</a>'
+ '</div>';
$(code).appendTo('body').animate(CONST.showAnimation, CONST.stayAtBottom);
return true;
},
_showFinishDialog: function() {
if(this._showSkippedQuestionsNagDialog()) return;
var sum = this.answersGiven.correct.length + this.answersGiven.fail.length;
var code = '<div style="display:none;" class="box hideMeOnMore">'
+ '<h3>Fertig!</h3>'
+ '<p>Du hast den aktuellen Block abgeschlossen. Insgesamt hast Du '+sum+' Fragen beantwortet und davon '+this.answersGiven.correct.length+' richtig. Scrolle nach oben um jeweils die Antworten für die Fragen zu sehen.</p>'
+ '<div class="button-group">'
+ '<a onclick="window.currentHitme.giveMore();" class="button big">Gib mir nochmal '+$('#quantity').val()+'!</a>'
+ '<a href="'+Routes.main_hitme_path()+'" class="button big">Einstellungen ändern</a>'
+ '</div>';
if($('#comiccheckbox').is(':checked')) {
code = code + XkcdLoader.retrieveWithText();
}
$(code).appendTo('body').animate(CONST.showAnimation, CONST.stayAtBottom);
animateVisibilityHiddenShow($('.reveal .answer-chooser > div > span:nth-child(2), .reveal .initiallyHidden .displayHidden'));
animateDisplayNoneShow($('.displayHidden'));
var allCorr = this.answersGiven.correct.diff(this.answersGiven.fail).join(',');
var anyFail = this.answersGiven.fail.join(',');
$(allCorr).addClass('correct');
$(anyFail).addClass('wrong');
},
showNext: function() {
if(this.questions.length === 0) {
alert("Keine Fragen für die aktuellen Einstellungen gefunden.\n\nVersuche es mit anderen Einstellungen nochmal.");
enableOptions();
showAllCategories();
// no more available questions?
} else if(this.questions.length-1 === this.questPositionPointer) {
this._showFinishDialog();
// reached user defined question limit
} else if(answersGivenCount >= parseInt($('#quantity').val())) {
this._showFinishDialog();
} else {
this._showNextQuestion();
}
},
skipCurrentQuestion: function(self) {
$(self).animate(CONST.hideAnimation);
this.showNext();
}
}
// convenience generator
H.hitme = function() {
var h = new H.Hitme();
window.currentHitme = h;
h.setupCategoryQuestionMode();
return h;
}
$(document).ready(function(){
if(isMobileBrowser) {
$("#quantity").attr("min", "1").attr("value", "2");
}
});