'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 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 = Object.keys(questionGroups).map( idx => {
45 let qg = questionGroups[idx];
46 const i = parseInt(idx);
47 // Re-order questions 1.1.1 then 1.1.2 then...
48 const orderedQg = qg.sort( (a,b) => {
49 let aParts = a.split('.').map(Number);
50 let bParts = b.split('.').map(Number);
51 const La = aParts.length, Lb = bParts.length;
52 for (let i=0; i<Math.min(La,Lb); i++)
53 {
54 if (aParts[i] != bParts[i])
55 return aParts[i] - bParts[i];
56 }
57 return La - Lb; //the longer should appear after
58 });
59 let qgDom = orderedQg.map( q => {
60 let questionContent = [ ];
61 questionContent.push(
62 h(
63 "h4",
64 {
65 "class": {
66 "questionIndex": true,
67 }
68 },
69 q.index
70 )
71 );
72 questionContent.push(
73 h(
74 "div",
75 {
76 "class": {
77 wording: true,
78 },
79 domProps: {
80 innerHTML: q.wording,
81 },
82 }
83 )
84 );
85 if (!!q.options)
86 {
87 // quiz-like question
88 let optionsOrder = _.range(q.options.length);
89 if (!q.fixed)
90 optionsOrder = _.shuffle(optionsOrder);
91 let optionList = [ ];
92 optionsOrder.forEach( idx => {
93 let option = [ ];
94 option.push(
95 h(
96 "input",
97 {
98 domProps: {
99 checked: !!this.inputs && this.inputs[q.index][idx],
100 disabled: monitoring || this.display == "solution",
101 },
102 attrs: {
103 id: this.inputId(q.index,idx),
104 type: "checkbox",
105 },
106 on: {
107 change: e => { this.inputs[q.index][idx] = e.target.checked; },
108 },
109 },
110 [ '' ] //to work in Firefox 45.9 ESR @ ENSTA...
111 )
112 );
113 option.push(
114 h(
115 "label",
116 {
117 domProps: {
118 innerHTML: q.options[idx],
119 },
120 attrs: {
121 "for": this.inputId(q.index,idx),
122 },
123 }
124 )
125 );
126 const aIdx = (this.answers || [ ]).findIndex( item => { return item.index == q.index; });
127 optionList.push(
128 h(
129 "div",
130 {
131 "class": {
132 option: true,
133 choiceCorrect: this.display == "solution" && this.answers[aIdx].includes(idx),
134 choiceWrong: this.display == "solution" && !!this.inputs && this.inputs[q.index][idx] && !this.answers[aIdx].includes(idx),
135 },
136 },
137 option
138 )
139 );
140 });
141 questionContent.push(
142 h(
143 "div",
144 {
145 "class": {
146 optionList: true,
147 },
148 },
149 optionList
150 )
151 );
152 }
153 else
154 {
155 // Open question, or parameterized: TODO
156 }
157 const depth = (q.index.match(/\./g) || []).length;
158 return h(
159 "div",
160 {
161 "class": {
162 "question": true,
163 ["depth" + depth]: true,
164 },
165 },
166 questionContent
167 );
168 });
169 return h(
170 "div",
171 {
172 "class": {
173 "questionGroup": true,
174 "hide": this.display == "one" && this.iidx != i,
175 },
176 },
177 qgDom
178 );
179 });
180 const navigator = h(
181 "div",
182 {
183 "class": {
184 "hide": this.displayStyle == "all"
185 },
186 },
187 [
188 h(
189 "button",
190 {
191 "class": {
192 "btn": true,
193 },
194 on: {
195 click: () => {
196 this.index = Math.max(0, this.index - 1);
197 },
198 },
199 },
200 [ h("span", { "class": { "material-icon": true } }, "fast_rewind") ]
201 ),
202 h("span",{ },(this.iidx+1).toString()),
203 h(
204 "button",
205 {
206 "class": {
207 "btn": true,
208 },
209 on: {
210 click: () => {
211 this.index = Math.min(this.index+1, this.questions.length-1)
212 },
213 },
214 },
215 [ h("span", { "class": { "material-icon": true } }, "fast_forward") ]
216 )
217 ]
218 );
219 domTree.push(navigator);
220 domTree.push(
221 h(
222 "button",
223 {
224 on: {
225 click: () => {
226 this.displayStyle = displayStyle == "compact" ? "all" : "compact";
227 },
228 },
229 },
230 this.displayStyle == "compact" ? "Show all" : "Navigator"
231 )
232 );
233 return h(
234 "div",
235 {
236 attrs: {
237 id: "statements",
238 },
239 },
240 domTree
241 );
242 },
243 mounted: function() {
244 statementsLibsRefresh();
245 },
246 updated: function() {
247 statementsLibsRefresh();
248 },
249 methods: {
250 inputId: function(i,j) {
251 return "q" + i + "_" + "input" + j;
252 },
253 },
254 });