X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=public%2Fjavascripts%2Fcomponents%2Fstatements.js;h=86138c00a2d2c7e72a6b5aff591756a9c41ead77;hb=14c9c66a3bc1abfeb3097a0c9fe4f0ff25fe2b8b;hp=6ebea85f1c94b55d1a7cd6f3083afe6ec16c1456;hpb=8a51dbf760226cbe6bddc44cddef2262c4945f5d;p=qomet.git diff --git a/public/javascripts/components/statements.js b/public/javascripts/components/statements.js index 6ebea85..86138c0 100644 --- a/public/javascripts/components/statements.js +++ b/public/javascripts/components/statements.js @@ -1,104 +1,233 @@ +/* + * questions group by index prefix 1.2.3 1.1 ...etc --> '1' + +NOTE: questions can contain parameterized exercises (how ? +--> describe variables (syntax ?) +--> write javascript script (OK, users trusted ? ==> safe mode possible if public website) +Imaginary example: (using math.js) + (avant l'exo) + x: math.random() + y: math.random() + M: math.matrix([[7, x], [y, -3]]); + res: math.det(M) + +
Calculer le déterminant de + $$\begin{matrix}7 & x\\y & -3\end{matrix}$$
+ * ... + ++ fixed + question time (syntax ?) + +--> input of type text (number, or vector, or matrix e.g. in R syntax) +--> parameter stored in question.param (TODO) + +*/ + Vue.component("statements", { - // 'answers' is an object containing - // 'inputs'(array), - // 'displayAll'(bool), - // 'showSolution'(bool), - // 'indices': order of appearance - // 'index': current integer index (focused question) - props: ['questions','answers'], - // TODO: general render function for nested exercises - // There should be a questions navigator below, or next (visible if display=='all') + // 'inputs': object with key = question index and value = text or boolean array + // display: 'all', 'one', 'solution' + // iidx: current level-0 integer index (can match a group of questions / inputs) + props: ['questions','inputs','answers','display','iidx'], + data: function() { + return { + displayStyle: "compact", //or "all": all on same page + parameters: 0, //TODO: DO NOT re-draw parameters for already answered questions + }; + } // Full questions tree is rendered, but some parts hidden depending on display settings render(h) { - // TODO: render nothing if answers is empty - let domTree = this.questions.map( (q,i) => { - let questionContent = [ ]; - questionContent.push( - h( - "div", - { - "class": { - wording: true, - }, - domProps: { - innerHTML: q.wording, - }, - } - ) - ); - let optionsOrder = _.range(q.options.length); - if (!q.fixed) - optionsOrder = _.shuffle(optionsOrder); - let optionList = [ ]; - optionsOrder.forEach( idx => { - let option = [ ]; - option.push( + // Prepare questions groups, ordered + let questions = this.questions || [ ] + let questionGroups = _.groupBy(questions, q => { + const dotPos = q.index.indexOf("."); + return dotPos === -1 ? q.index : q.index.substring(0,dotPos); + }); + let domTree = questionGroups.map( (qg,i) => { + // Re-order questions 1.1.1 then 1.1.2 then... + const orderedQg = qg.sort( (a,b) => { + let aParts = a.split('.').map(Number); + let bParts = b.split('.').map(Number); + const La = aParts.length, Lb = bParts.length; + for (let i=0; i { + let questionContent = [ ]; + questionContent.push( h( - "input", + "h4", { - domProps: { - checked: this.answers.inputs.length > 0 && this.answers.inputs[i][idx], - disabled: monitoring, - }, - attrs: { - id: this.inputId(i,idx), - type: "checkbox", - }, - on: { - change: e => { this.answers.inputs[i][idx] = e.target.checked; }, - }, + "class": { + "questionIndex": true, + } }, + q.index ) ); - option.push( + questionContent.push( h( - "label", + "div", { - domProps: { - innerHTML: q.options[idx], + "class": { + wording: true, }, - attrs: { - "for": this.inputId(i,idx), + domProps: { + innerHTML: q.wording, }, } ) ); - optionList.push( - h( - "div", - { - "class": { - option: true, - choiceCorrect: this.answers.showSolution && this.questions[i].answer.includes(idx), - choiceWrong: this.answers.showSolution && this.inputs[i][idx] && !q.answer.includes(idx), + if (!!q.options) + { + // quiz-like question + let optionsOrder = _.range(q.options.length); + if (!q.fixed) + optionsOrder = _.shuffle(optionsOrder); + let optionList = [ ]; + optionsOrder.forEach( idx => { + let option = [ ]; + option.push( + h( + "input", + { + domProps: { + checked: !!this.inputs && this.inputs[q.index][idx], + disabled: monitoring || this.display == "solution", + }, + attrs: { + id: this.inputId(q.index,idx), + type: "checkbox", + }, + on: { + change: e => { this.inputs[q.index][idx] = e.target.checked; }, + }, + }, + [ '' ] //to work in Firefox 45.9 ESR @ ENSTA... + ) + ); + option.push( + h( + "label", + { + domProps: { + innerHTML: q.options[idx], + }, + attrs: { + "for": this.inputId(q.index,idx), + }, + } + ) + ); + const aIdx = (this.answers || [ ]).findIndex( item => { return item.index == q.index; }); + optionList.push( + h( + "div", + { + "class": { + option: true, + choiceCorrect: this.display == "solution" && this.answers[aIdx].includes(idx), + choiceWrong: this.display == "solution" && !!this.inputs && this.inputs[q.index][idx] && !this.answers[aIdx].includes(idx), + }, + }, + option + ) + ); + }); + questionContent.push( + h( + "div", + { + "class": { + optionList: true, + }, }, - }, - option - ) - ); - }); - questionContent.push( - h( + optionList + ) + ); + } + else + { + // Open question, or parameterized: TODO + } + const depth = (q.index.match(/\./g) || []).length; + return h( "div", { "class": { - optionList: true, + "question": true, + "depth" + depth: true, }, }, - optionList - ) - ); + questionContent + ); + }); return h( "div", { "class": { - "question": true, - "hide": !this.answers.displayAll && this.answers.index != i, + "questionGroup": true, + "hide": this.display == "one" && this.iidx != i, }, - }, - questionContent + } + qgDom ); }); + const navigator = h( + "div", + { + "class": { + "hide": this.displayStyle == "all" + }, + }, + [ + h( + "button", + { + "class": { + "btn": true, + }, + on: { + click: () => { + this.index = Math.max(0, this.index - 1); + }, + }, + }, + [ h("span", { "class": { "material-icon": true } }, "fast_rewind") ] + ), + h("span",{ },(this.iidx+1).toString()), + h( + "button", + { + "class": { + "btn": true, + }, + on: { + click: () => { + this.index = Math.min(this.index+1, this.questions.length-1) + }, + }, + }, + [ h("span", { "class": { "material-icon": true } }, "fast_forward") ] + ) + ] + ); + domTree.push(navigator); + domTree.push( + h( + "button", + { + on: { + click: () => { + this.displayStyle = displayStyle == "compact" ? "all" : "compact"; + }, + }, + }, + this.displayStyle == "compact" ? "Show all" : "Navigator" + ) + ); return h( "div", { @@ -106,12 +235,13 @@ Vue.component("statements", { id: "statements", }, }, - questions + domTree ); }, + mounted: function() { + statementsLibsRefresh(); + }, updated: function() { - // TODO: next line shouldn't be required: questions wordings + answer + options - // are processed earlier; their content should be updated at this time. statementsLibsRefresh(); }, methods: {