'update'
[qomet.git] / public / javascripts / components / statements.js
index 8cfbde3..86138c0 100644 (file)
+/*
+ * 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)
+       <params> (avant l'exo)
+       x: math.random()
+       y: math.random()
+       M: math.matrix([[7, x], [y, -3]]);
+       res: math.det(M)
+       </params>
+       <div>Calculer le déterminant de 
+       $$\begin{matrix}7 & x\\y & -3\end{matrix}$$</div>
+       * ...
+
++ 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<Math.min(La,Lb); i++)
+                               {
+                                       if (aParts[i] != bParts[i])
+                                               return aParts[i] - bParts[i];
+                               }
+                               return La - Lb; //the longer should appear after
+                       });
+                       let qgDom = orderedQg.map( q => {
+                               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.answers.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.indices[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",
                        {
@@ -113,8 +242,6 @@ Vue.component("statements", {
                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: {