86138c00a2d2c7e72a6b5aff591756a9c41ead77
[qomet.git] / public / javascripts / components / statements.js
1 /*
2 * questions group by index prefix 1.2.3 1.1 ...etc --> '1'
3
4 NOTE: questions can contain parameterized exercises (how ?
5 --> describe variables (syntax ?)
6 --> write javascript script (OK, users trusted ? ==> safe mode possible if public website)
7 Imaginary example: (using math.js)
8 <params> (avant l'exo)
9 x: math.random()
10 y: math.random()
11 M: math.matrix([[7, x], [y, -3]]);
12 res: math.det(M)
13 </params>
14 <div>Calculer le déterminant de
15 $$\begin{matrix}7 & x\\y & -3\end{matrix}$$</div>
16 * ...
17
18 + fixed + question time (syntax ?)
19
20 --> input of type text (number, or vector, or matrix e.g. in R syntax)
21 --> parameter stored in question.param (TODO)
22
23 */
24
25 Vue.component("statements", {
26 // 'inputs': object with key = question index and value = text or boolean array
27 // display: 'all', 'one', 'solution'
28 // iidx: current level-0 integer index (can match a group of questions / inputs)
29 props: ['questions','inputs','answers','display','iidx'],
30 data: function() {
31 return {
32 displayStyle: "compact", //or "all": all on same page
33 parameters: 0, //TODO: DO NOT re-draw parameters for already answered questions
34 };
35 }
36 // Full questions tree is rendered, but some parts hidden depending on display settings
37 render(h) {
38 // Prepare questions groups, ordered
39 let questions = this.questions || [ ]
40 let questionGroups = _.groupBy(questions, q => {
41 const dotPos = q.index.indexOf(".");
42 return dotPos === -1 ? q.index : q.index.substring(0,dotPos);
43 });
44 let domTree = questionGroups.map( (qg,i) => {
45 // Re-order questions 1.1.1 then 1.1.2 then...
46 const orderedQg = qg.sort( (a,b) => {
47 let aParts = a.split('.').map(Number);
48 let bParts = b.split('.').map(Number);
49 const La = aParts.length, Lb = bParts.length;
50 for (let i=0; i<Math.min(La,Lb); i++)
51 {
52 if (aParts[i] != bParts[i])
53 return aParts[i] - bParts[i];
54 }
55 return La - Lb; //the longer should appear after
56 });
57 let qgDom = orderedQg.map( q => {
58 let questionContent = [ ];
59 questionContent.push(
60 h(
61 "h4",
62 {
63 "class": {
64 "questionIndex": true,
65 }
66 },
67 q.index
68 )
69 );
70 questionContent.push(
71 h(
72 "div",
73 {
74 "class": {
75 wording: true,
76 },
77 domProps: {
78 innerHTML: q.wording,
79 },
80 }
81 )
82 );
83 if (!!q.options)
84 {
85 // quiz-like question
86 let optionsOrder = _.range(q.options.length);
87 if (!q.fixed)
88 optionsOrder = _.shuffle(optionsOrder);
89 let optionList = [ ];
90 optionsOrder.forEach( idx => {
91 let option = [ ];
92 option.push(
93 h(
94 "input",
95 {
96 domProps: {
97 checked: !!this.inputs && this.inputs[q.index][idx],
98 disabled: monitoring || this.display == "solution",
99 },
100 attrs: {
101 id: this.inputId(q.index,idx),
102 type: "checkbox",
103 },
104 on: {
105 change: e => { this.inputs[q.index][idx] = e.target.checked; },
106 },
107 },
108 [ '' ] //to work in Firefox 45.9 ESR @ ENSTA...
109 )
110 );
111 option.push(
112 h(
113 "label",
114 {
115 domProps: {
116 innerHTML: q.options[idx],
117 },
118 attrs: {
119 "for": this.inputId(q.index,idx),
120 },
121 }
122 )
123 );
124 const aIdx = (this.answers || [ ]).findIndex( item => { return item.index == q.index; });
125 optionList.push(
126 h(
127 "div",
128 {
129 "class": {
130 option: true,
131 choiceCorrect: this.display == "solution" && this.answers[aIdx].includes(idx),
132 choiceWrong: this.display == "solution" && !!this.inputs && this.inputs[q.index][idx] && !this.answers[aIdx].includes(idx),
133 },
134 },
135 option
136 )
137 );
138 });
139 questionContent.push(
140 h(
141 "div",
142 {
143 "class": {
144 optionList: true,
145 },
146 },
147 optionList
148 )
149 );
150 }
151 else
152 {
153 // Open question, or parameterized: TODO
154 }
155 const depth = (q.index.match(/\./g) || []).length;
156 return h(
157 "div",
158 {
159 "class": {
160 "question": true,
161 "depth" + depth: true,
162 },
163 },
164 questionContent
165 );
166 });
167 return h(
168 "div",
169 {
170 "class": {
171 "questionGroup": true,
172 "hide": this.display == "one" && this.iidx != i,
173 },
174 }
175 qgDom
176 );
177 });
178 const navigator = h(
179 "div",
180 {
181 "class": {
182 "hide": this.displayStyle == "all"
183 },
184 },
185 [
186 h(
187 "button",
188 {
189 "class": {
190 "btn": true,
191 },
192 on: {
193 click: () => {
194 this.index = Math.max(0, this.index - 1);
195 },
196 },
197 },
198 [ h("span", { "class": { "material-icon": true } }, "fast_rewind") ]
199 ),
200 h("span",{ },(this.iidx+1).toString()),
201 h(
202 "button",
203 {
204 "class": {
205 "btn": true,
206 },
207 on: {
208 click: () => {
209 this.index = Math.min(this.index+1, this.questions.length-1)
210 },
211 },
212 },
213 [ h("span", { "class": { "material-icon": true } }, "fast_forward") ]
214 )
215 ]
216 );
217 domTree.push(navigator);
218 domTree.push(
219 h(
220 "button",
221 {
222 on: {
223 click: () => {
224 this.displayStyle = displayStyle == "compact" ? "all" : "compact";
225 },
226 },
227 },
228 this.displayStyle == "compact" ? "Show all" : "Navigator"
229 )
230 );
231 return h(
232 "div",
233 {
234 attrs: {
235 id: "statements",
236 },
237 },
238 domTree
239 );
240 },
241 mounted: function() {
242 statementsLibsRefresh();
243 },
244 updated: function() {
245 statementsLibsRefresh();
246 },
247 methods: {
248 inputId: function(i,j) {
249 return "q" + i + "_" + "input" + j;
250 },
251 },
252 });