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