18
18
import $ from "jquery" ;
19
19
import "theme_boost/bootstrap/popover" ;
20
20
21
+ /**
22
+ * @type {?Attempt } Attempt object that is passed to the question package.
23
+ */
24
+ let attempt = null ;
25
+
21
26
/**
22
27
* If the given input(-like) element is labelled, returns the label element. Returns null otherwise.
23
28
*
@@ -78,7 +83,8 @@ function markInvalid(element, message, ariaInvalid = true) {
78
83
$ ( popoverTarget ) . popover ( {
79
84
toggle : "popover" ,
80
85
trigger : "hover" ,
81
- content : message
86
+ placement : "bottom" ,
87
+ content : message ,
82
88
} ) ;
83
89
}
84
90
@@ -132,9 +138,15 @@ async function checkConstraints(element) {
132
138
}
133
139
134
140
/**
135
- * Adds change event handlers for soft validation.
141
+ * Initializes the question.
142
+ *
143
+ * This function must be called within the iframe.
144
+ *
145
+ * @param {string } autoSaveHintInputId
146
+ * @param {string[] } roles QPy role names that the user has.
136
147
*/
137
- export async function init ( ) {
148
+ export async function init ( autoSaveHintInputId , roles ) {
149
+ // Add change event handlers for soft validation.
138
150
for ( const element of document . querySelectorAll ( `
139
151
[data-qpy_required], [data-qpy_pattern],
140
152
[data-qpy_minlength], [data-qpy_maxlength],
@@ -143,4 +155,141 @@ export async function init() {
143
155
await checkConstraints ( element ) ;
144
156
element . addEventListener ( "change" , event => checkConstraints ( event . target ) ) ;
145
157
}
158
+
159
+ const form = window . document . getElementById ( "qpy-formulation" ) ;
160
+ if ( form ) {
161
+ // On form submit, submit the quiz's main form in the parent window instead.
162
+ form . addEventListener ( "submit" , event => {
163
+ event . preventDefault ( ) ;
164
+ window . frameElement . closest ( "form" ) . submit ( ) ;
165
+ } ) ;
166
+
167
+ // Modify a field in the main form in order to tell the Quiz's autosaver that the user changed an answer.
168
+ const autoSaveHintElement = parent . document . getElementById ( autoSaveHintInputId ) ;
169
+ if ( autoSaveHintElement ) {
170
+ form . addEventListener ( "change" , function ( ) {
171
+ autoSaveHintElement . value = parseInt ( autoSaveHintElement . value ) + 1 ;
172
+ } ) ;
173
+ }
174
+ }
175
+
176
+ // Attempt object that is passed to the question package.
177
+ attempt = new Attempt (
178
+ window . document . getElementById ( "qpy-formulation" ) ,
179
+ window . document . getElementById ( "qpy-general-feedback" ) ,
180
+ window . document . getElementById ( "qpy-specific-feedback" ) ,
181
+ window . document . getElementById ( "qpy-right-answer" ) ,
182
+ roles
183
+ ) ;
184
+ }
185
+
186
+ /**
187
+ * Get a QuestionPy attempt.
188
+ *
189
+ * @returns {Attempt }
190
+ */
191
+ export function getAttempt ( ) {
192
+ if ( attempt === null ) {
193
+ throw new Error ( "Attempt not initialized" ) ;
194
+ }
195
+ return attempt ;
196
+ }
197
+
198
+ class Attempt {
199
+ #formulation;
200
+ #generalFeedback;
201
+ #specificFeedback;
202
+ #rightAnswer;
203
+ #roles;
204
+
205
+ /**
206
+ * @param {Element } formulationElement
207
+ * @param {?Element } generalFeedbackElement
208
+ * @param {?Element } specificFeedbackElement
209
+ * @param {?Element } rightAnswer
210
+ * @param {string[] } roles
211
+ */
212
+ constructor ( formulationElement , generalFeedbackElement , specificFeedbackElement , rightAnswer , roles ) {
213
+ this . #formulation = formulationElement ;
214
+ this . #generalFeedback = generalFeedbackElement ;
215
+ this . #specificFeedback = specificFeedbackElement ;
216
+ this . #rightAnswer = rightAnswer ;
217
+ this . #roles = roles ;
218
+ }
219
+
220
+ /**
221
+ * Get the top html element where the question's formulation xhtml was inserted.
222
+ *
223
+ * @returns {Element }
224
+ */
225
+ get formulationElement ( ) {
226
+ return this . #formulation;
227
+ }
228
+
229
+ /**
230
+ * Get the top html element where the question's general feedback xhtml was inserted (if available).
231
+ *
232
+ * @returns {?Element }
233
+ */
234
+ get generalFeedbackElement ( ) {
235
+ return this . #generalFeedback;
236
+ }
237
+
238
+ /**
239
+ * Get the top html element where the question's specific feedback xhtml was inserted (if available).
240
+ *
241
+ * @returns {?Element }
242
+ */
243
+ get specificFeedbackElement ( ) {
244
+ return this . #specificFeedback;
245
+ }
246
+
247
+ /**
248
+ * Get the top html element where the question's right answer xhtml was inserted (if available).
249
+ *
250
+ * @returns {?Element }
251
+ */
252
+ get rightAnswerElement ( ) {
253
+ return this . #rightAnswer;
254
+ }
255
+
256
+ /**
257
+ * Get the names of the roles that the current user has.
258
+ *
259
+ * @typedef {'teacher' | 'developer' | 'scorer' | 'proctor' } roleName
260
+ * @returns {roleName[] }
261
+ */
262
+ get userRoles ( ) {
263
+ return this . #roles;
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Add the question's form data located in the iframe to the main form when it is submitted.
269
+ *
270
+ * This function must be called outside the iframe, on the parent window.
271
+ *
272
+ * @param {string } iframeId - The ID of the question's iframe.
273
+ * @param {string } fieldPrefix - The prefix to add to the field names, for Moodle to recognize the fields belonging to a question.
274
+ * @return {void } This function does not return a value.
275
+ */
276
+ export function addIframeFormDataOnSubmit ( iframeId , fieldPrefix ) {
277
+ const iframe = window . document . getElementById ( iframeId ) ;
278
+ if ( iframe === null ) {
279
+ window . console . error ( `Could not find question iframe ${ iframeId } . Cannot save answers.` ) ;
280
+ return ;
281
+ }
282
+
283
+ const form = iframe . closest ( "form" ) ;
284
+ form . addEventListener ( "formdata" , event => {
285
+ const iframeForm = iframe . contentDocument . getElementById ( "qpy-formulation" ) ;
286
+ if ( iframeForm === null ) {
287
+ window . console . error ( "Could not find form in question iframe " + iframeId ) ;
288
+ return ;
289
+ }
290
+ const iframeFormData = new FormData ( iframeForm ) ;
291
+ for ( const [ key , value ] of iframeFormData ) {
292
+ event . formData . append ( fieldPrefix + key , value ) ;
293
+ }
294
+ } ) ;
146
295
}
0 commit comments