@@ -63,7 +63,9 @@ function validateInput(element) {
6363 * @param {boolean } showSpecificFeedback
6464 * @param {boolean } showRightAnswer
6565 * @param {boolean } showCorrectness
66- * @param {string } responseId
66+ * @param {string } responseId Id of the hidden input containing the JSON-encoded main response.
67+ * @param {string } editorsId Id of the hidden input containing the JSON-encoded WYSIWYG editors data.
68+ * @param {string[] } editorNames Names of the WYSIWYG editors.
6769 * @param {string[] } roles QPy role names that the user has.
6870 * @param {Object.<string, any> } data Dynamic data.
6971 * @param {Number } environmentVersion
@@ -75,6 +77,8 @@ export async function init(
7577 showRightAnswer ,
7678 showCorrectness ,
7779 responseId ,
80+ editorsId ,
81+ editorNames ,
7882 roles ,
7983 data ,
8084 environmentVersion ,
@@ -102,10 +106,17 @@ export async function init(
102106
103107 // Modify a field in the main form in order to tell the Quiz's autosaver that the user changed an answer.
104108 const responseElement = parent . document . getElementById ( responseId ) ;
105- if ( responseElement ) {
109+ const editorsElement = parent . document . getElementById ( editorsId ) ;
110+ if ( responseElement || editorsElement ) {
106111 // We throttle here, as `JSON.stringify` might affect the performance.
107112 form . addEventListener ( "change" , throttle ( ( ) => {
108- responseElement . value = createJsonFromFormData ( form ) ;
113+ const [ responseData , editorsData ] = collectFormData ( form , editorNames ) ;
114+ if ( responseElement ) {
115+ responseElement . value = responseData ;
116+ }
117+ if ( editorsElement ) {
118+ editorsElement . value = editorsData ;
119+ }
109120 } , 250 ) ) ;
110121 }
111122
@@ -403,29 +414,73 @@ class Attempt {
403414 }
404415}
405416
417+ function buildDraftFileUrlRegex ( ) {
418+ const wwwrootWithoutScheme = M . cfg . wwwroot . replace ( / ^ h t t p s ? : \/ \/ / , "" ) ;
419+
420+ return new RegExp (
421+ // Phpcs:disable -- phpcs is massively confused by this.
422+ String . raw `https?://${ wwwrootWithoutScheme } /draftfile\.php/(?<contextid>\d+)`
423+ + String . raw `/user/draft/(?<itemid>\d+)/(?<filename>[^\'\",&<>|\`\s:\\\\]+)`
424+ // Phpcs:enable
425+ )
426+ }
427+
406428/**
407429 * Creates JSON from the FormData of the given form.
408430 *
409431 * @param {HTMLFormElement } form
410- * @returns {string }
432+ * @param {string[] } editorNames
433+ * @returns {[string, string] }
411434 */
412- function createJsonFromFormData ( form ) {
435+ function collectFormData ( form , editorNames ) {
413436 const iframeFormData = new FormData ( form ) ;
414- const iframeObject = Object . fromEntries ( iframeFormData ) ;
437+
438+ const editorData = { } ;
439+ for ( const name of editorNames ) {
440+ // TODO: Turn draftfile.php-URLs into @@PLUGINFILE@@-URLs.
441+
442+ const textKey = `${ name } [text]` ;
443+ const formatKey = `${ name } [format]` ;
444+ const itemidKey = `${ name } [itemid]` ;
445+
446+ const text = iframeFormData . get ( textKey ) ;
447+ if ( text === null ) {
448+ continue ;
449+ }
450+
451+ // TODO: Handle content pasted from other editors, where the draft item id would be different. We'd probably
452+ // need to pass the encountered foreign files somewhere and copy them to our area in qbehaviour_questionpy.
453+ const replacedText = text . replaceAll ( buildDraftFileUrlRegex ( ) , "@@PLUGINFILE@@/$<filename>" ) ;
454+
455+ editorData [ name ] = { text : replacedText } ;
456+ iframeFormData . delete ( textKey ) ;
457+
458+ const format = iframeFormData . get ( formatKey ) ;
459+ if ( format !== null ) {
460+ editorData [ name ] . format = format ;
461+ iframeFormData . delete ( formatKey ) ;
462+ }
463+
464+ // The itemid is added by qpy_rich_text_editor to the list that gets sent from outside the iframe.
465+ // No need to duplicate it here.
466+ iframeFormData . delete ( itemidKey ) ;
467+ }
468+
469+ const responseObject = Object . fromEntries ( iframeFormData ) ;
415470 for ( const name of iframeFormData . keys ( ) ) {
416471 const values = iframeFormData . getAll ( name ) ;
417472 if ( values . length > 1 ) {
418- iframeObject [ name ] = values ;
473+ responseObject [ name ] = values ;
419474 }
420475 }
421476
422- if ( iframeObject . data ) {
423- iframeObject . data = JSON . parse ( iframeObject . data ) ;
477+ if ( responseObject . data ) {
478+ responseObject . data = JSON . parse ( responseObject . data ) ;
424479 } else {
425480 window . console . warn ( "The form data field 'data' is missing in the question iframe form." ) ;
426481 }
427482
428- return JSON . stringify ( iframeObject ) ;
483+ return [ JSON . stringify ( responseObject ) , JSON . stringify ( editorData ) ] ;
429484}
430485
431486/**
@@ -435,8 +490,10 @@ function createJsonFromFormData(form) {
435490 *
436491 * @param {string } iframeId - The ID of the question's iframe.
437492 * @param {string } responseFieldName - The complete field name for the JSON-encoded iframe form data.
493+ * @param {string } editorsFieldName - The complete field name for the JSON-encoded WYSIWYG editors data.
494+ * @param {string[] } editorNames - The input names that are WYSIWYG editors.
438495 */
439- export function addIframeFormDataOnSubmit ( iframeId , responseFieldName ) {
496+ export function addIframeFormDataOnSubmit ( iframeId , responseFieldName , editorsFieldName , editorNames ) {
440497 const iframe = window . document . getElementById ( iframeId ) ;
441498 if ( iframe === null ) {
442499 window . console . error ( `Could not find question iframe ${ iframeId } . Cannot save answers.` ) ;
@@ -450,9 +507,11 @@ export function addIframeFormDataOnSubmit(iframeId, responseFieldName) {
450507 window . console . error ( "Could not find form in question iframe " + iframeId ) ;
451508 return ;
452509 }
510+
453511 // Since we are throttling the updating process of the response element on a change, it might happen that the
454512 // value is outdated - this is why we get the data again.
455- const jsonFormData = createJsonFromFormData ( iframeForm ) ;
456- event . formData . set ( responseFieldName , jsonFormData ) ;
513+ const [ responseData , editorsData ] = collectFormData ( iframeForm , editorNames ) ;
514+ event . formData . set ( responseFieldName , responseData ) ;
515+ event . formData . set ( editorsFieldName , editorsData ) ;
457516 } ) ;
458517}
0 commit comments