diff --git a/README.md b/README.md index 4011df8..948f331 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ guide the learner’s interaction with the component. **\_recordInteraction** (boolean) Determines whether or not the learner's answers will be recorded to the LMS via cmi.interactions. Default is `true`. For further information, see the entry for `_shouldRecordInteractions` in the README for [adapt-contrib-spoor](https://github.com/adaptlearning/adapt-contrib-spoor). -**\_items** (array): Each *item* represents one choice for the multiple choice question and contains values for **text**, **\_shouldBeSelected**, and **feedback**. +**\_items** (array): Each *item* represents one choice for the multiple choice question and contains values for **text**, **\_shouldBeSelected**, and optionally **feedback**. >**text** (string): Text that comprises the multiple choice option. @@ -78,30 +78,69 @@ Optional for providing alternative text, for example, to specify how a word shou >**\_isPartlyCorrect** (boolean): Determines whether the *item* when selected marks the question as partly correct. Value can be `true` or `false`. Default is `false`. ->**feedback** (string): This attribute is used only when the value for **\_selectable** is set to `1` (i.e., radio button style questions). This text will be shown if the learner selects this item, and it is an incorrect answer. +>**feedback** (object): Used only when **\_selectable** is set to `1` (radio button style). When the learner selects this item and it is an incorrect answer, this object overrides the component-level **\_feedback** state. A plain string is also accepted (legacy) and is used as the body text only. + +>>**title** (string): Feedback title for this item. Overrides the component-level `_feedback.title`. + +>>**altTitle** (string): Alternative title read by screen readers if no visual title is included. + +>>**body** (string): The feedback body text for this item. + +>>**\_classes** (string): CSS class name(s) to be applied to the feedback panel for this item. + +>>**\_imageAlignment** (string): Alignment of the graphic relative to the feedback text. Acceptable values are `left` and `right`. Defaults to `right`. + +>>**\_graphic** (object): An optional image to display in the feedback panel for this item. + +>>>**\_src** (string): File path (relative to the `course` folder) to the image. + +>>>**alt** (string): A description of the image for screen readers. + +>>>**attribution** (string): Optional text to be displayed as an attribution. >**\_score** (number): If `_hasItemScoring` is `true`, when selected, item scores are summed to give the question score. -**\_feedback** (object): If the [**Tutor** extension](https://github.com/adaptlearning/adapt-contrib-tutor) is enabled, these various texts will be displayed depending on the submitted answer. **\_feedback** -contains values for three types of answers: **correct**, **\_incorrect**, and **\_partlyCorrect**. Some attributes are optional. If they are not supplied, the default that is noted below will be used. +**\_feedback** (object): If the [**Tutor** extension](https://github.com/adaptlearning/adapt-contrib-tutor) is enabled, these texts will be displayed depending on the submitted answer. ->**title** (string): Title text for the feedback that will be displayed when the question is submitted. +>**title** (string): Default title text shown in the feedback modal header. Per-state objects can override this with their own `title` field; if a state's `title` is empty or absent, this value is used as the fallback. >**altTitle** (string): This will be read out by screen readers as an alternative title if no visual title is included. ->**correct** (string): Text that will be displayed when the submitted answer is correct. +>**\_classes** (string): CSS class name(s) to be applied to the feedback panel for all states. Separate multiple classes with a space. + +**Per-state feedback properties:** + +Each of the five properties below accepts the same feedback state object: + +>**\_correct** (object): Feedback displayed when the submitted answer is correct. + +>**\_incorrectFinal** (object): Feedback displayed when the submitted answer is incorrect and no more attempts are permitted. + +>**\_incorrectNotFinal** (object): Feedback displayed when the submitted answer is incorrect and attempts remain. If omitted, **\_incorrectFinal** is used instead. + +>**\_partlyCorrectFinal** (object): Feedback displayed when the submitted answer is partly correct and no more attempts are permitted. If omitted, **\_incorrectFinal** is used instead. + +>**\_partlyCorrectNotFinal** (object): Feedback displayed when the submitted answer is partly correct and attempts remain. If omitted, **\_partlyCorrectFinal** is used instead. + +Each feedback state object accepts the following properties: + +>>**title** (string): Title text for this feedback state. When non-empty, overrides the top-level `title`. When empty or absent, the top-level `title` is used as a fallback. + +>>**altTitle** (string): Alternative title text read out by screen readers if no visual title is included. + +>>**body** (string): The feedback body text for this state. ->**\_incorrect** (object): Texts that will be displayed when the submitted answer is incorrect. It contains values that are displayed under differing conditions: **final** and **notFinal**. +>>**\_classes** (string): CSS class name(s) to be applied to the feedback panel for this state only. Separate multiple classes with a space. ->>**final** (string): Text that will be displayed when the submitted answer is incorrect and no more attempts are permitted. +>>**\_imageAlignment** (string): The alignment of the graphic relative to the feedback text. Acceptable values are `left` and `right`. Defaults to `right`. ->>**notFinal** (string): Text that will be displayed when the submitted answer is incorrect while more attempts are permitted. This is optional—if you do not supply it, the **\_incorrect.final** feedback will be shown instead. +>>**\_graphic** (object): An optional image to display in the feedback panel for this state. ->**\_partlyCorrect** (object): Texts that will be displayed when the submitted answer is partially correct. It contains values that are displayed under differing conditions: **final** and **notFinal**. +>>>**\_src** (string): File path (relative to the `course` folder) to the image. Best practice is to include the file extension. ->>**final** (string): Text that will be displayed when the submitted answer is partly correct and no more attempts are permitted. This is optional—if you do not supply it, the **\_incorrect.final** feedback will be shown instead. +>>>**alt** (string): A description of the image for screen readers. ->>**notFinal** (string): Text that will be displayed when the submitted answer is partly correct while more attempts are permitted. This is optional—if you do not supply it, the **\_incorrect.notFinal** feedback will be shown instead. +>>>**attribution** (string): Optional text to be displayed as an attribution. ### Accessibility **Multiple Choice Question** has been assigned a descriptive label using the [aria-label](https://github.com/adaptlearning/adapt_framework/wiki/Aria-Labels) attribute: **ariaRegion**. diff --git a/example.json b/example.json index 30b5be5..8d5363a 100644 --- a/example.json +++ b/example.json @@ -1,89 +1,156 @@ - { - "_id": "c-05", - "_parentId": "b-05", - "_type": "component", - "_component": "mcq", - "_classes": "", - "_layout": "left", - "title": "MCQ", - "displayTitle": "MCQ", - "body": "Which of the following options would you consider to be correct?", - "instruction": "Choose {{#if _isRadio}}one option{{else}}one or more options{{/if}} then select Submit.", - "ariaQuestion": "", - "_attempts": 1, - "_shouldDisplayAttempts": false, - "_isRandom": false, - "_hasItemScoring": false, - "_questionWeight": 1, - "_selectable": 1, - "_canShowModelAnswer": true, - "_canShowCorrectness": false, - "_canShowFeedback": true, - "_canShowMarking": true, - "_recordInteraction": true, - "_items": [ - { - "text": "This is option 1 (Correct)", - "altText": "", - "_shouldBeSelected": true, - "_isPartlyCorrect": false - }, - { - "text": "This is option 2", - "_shouldBeSelected": false, - "feedback": "Option two incorrect feedback", - "_isPartlyCorrect": false - }, - { - "text": "This is option 3", - "_shouldBeSelected": false, - "_isPartlyCorrect": false - }, - { - "text": "This is option 4", - "_shouldBeSelected": false, - "_isPartlyCorrect": false - } - ], - "_feedback":{ - "title": "Feedback", - "altTitle": "Alt feedback title text", - "correct": "Congratulations, this is the correct feedback.", - "_incorrect": { - "notFinal": "", - "final": "This feedback will appear if you answered the question incorrectly." - }, - "_partlyCorrect": { - "notFinal": "", - "final": "This feedback will appear if you answered the question correctly." - } - }, - "_comment": "You only need to include _buttons if you want to override the button labels that are set in course.json", - "_buttons": { - "_submit": { - "buttonText": "Submit", - "ariaLabel": "Select here to submit your answer." - }, - "_reset": { - "buttonText": "Reset", - "ariaLabel": "" - }, - "_showCorrectAnswer": { - "buttonText": "Correct Answer", - "ariaLabel": "" - }, - "_hideCorrectAnswer": { - "buttonText": "My Answer", - "ariaLabel": "" - }, - "_showFeedback": { - "buttonText": "Show feedback", - "ariaLabel": "" - }, - "remainingAttemptsText": "attempts remaining", - "remainingAttemptText": "final attempt" + { + "_id": "c-05", + "_parentId": "b-05", + "_type": "component", + "_component": "mcq", + "_classes": "", + "_layout": "left", + "title": "MCQ", + "displayTitle": "MCQ", + "body": "Which of the following options would you consider to be correct?", + "instruction": "Choose {{#if _isRadio}}one option{{else}}one or more options{{/if}} then select Submit.", + "ariaQuestion": "", + "_attempts": 1, + "_shouldDisplayAttempts": false, + "_isRandom": false, + "_hasItemScoring": false, + "_questionWeight": 1, + "_selectable": 1, + "_canShowModelAnswer": true, + "_canShowCorrectness": false, + "_canShowFeedback": true, + "_canShowMarking": true, + "_recordInteraction": true, + "_items": [ + { + "text": "This is option 1 (Correct)", + "altText": "", + "_shouldBeSelected": true, + "_isPartlyCorrect": false + }, + { + "text": "This is option 2", + "_shouldBeSelected": false, + "feedback": { + "title": "Option two feedback title", + "altTitle": "", + "body": "Option two incorrect feedback", + "_classes": "", + "_imageAlignment": "right", + "_graphic": { + "_src": "", + "alt": "", + "attribution": "" + } }, - "_pageLevelProgress": { - "_isEnabled": true + "_isPartlyCorrect": false + }, + { + "text": "This is option 3", + "altText": "", + "_shouldBeSelected": false, + "_isPartlyCorrect": false + }, + { + "text": "This is option 4", + "altText": "", + "_shouldBeSelected": false, + "_isPartlyCorrect": false + } + ], + "_feedback": { + "title": "Feedback", + "altTitle": "", + "_classes": "", + "_correct": { + "title": "Feedback", + "altTitle": "", + "body": "Congratulations, this is the correct feedback.", + "_classes": "", + "_imageAlignment": "right", + "_graphic": { + "_src": "", + "alt": "", + "attribution": "" + } + }, + "_incorrectFinal": { + "title": "Feedback", + "altTitle": "", + "body": "This feedback will appear if you answered the question incorrectly.", + "_classes": "", + "_imageAlignment": "right", + "_graphic": { + "_src": "", + "alt": "", + "attribution": "" + } + }, + "_incorrectNotFinal": { + "_comment": "Optional. If omitted or body is empty, _incorrectFinal is shown instead.", + "title": "Feedback", + "altTitle": "", + "body": "", + "_classes": "", + "_imageAlignment": "right", + "_graphic": { + "_src": "", + "alt": "", + "attribution": "" + } + }, + "_partlyCorrectFinal": { + "title": "Feedback", + "altTitle": "", + "body": "This feedback will appear if you answered the question partly correctly.", + "_classes": "", + "_imageAlignment": "right", + "_graphic": { + "_src": "", + "alt": "", + "attribution": "" + } + }, + "_partlyCorrectNotFinal": { + "_comment": "Optional. If omitted or body is empty, _partlyCorrectFinal is shown instead; if that is also absent, _incorrectFinal is used.", + "title": "Feedback", + "altTitle": "", + "body": "", + "_classes": "", + "_imageAlignment": "right", + "_graphic": { + "_src": "", + "alt": "", + "attribution": "" } + } + }, + "_comment": "You only need to include _buttons if you want to override the button labels that are set in course.json", + "_buttons": { + "_submit": { + "buttonText": "Submit", + "ariaLabel": "Select here to submit your answer." + }, + "_reset": { + "buttonText": "Reset", + "ariaLabel": "" + }, + "_showCorrectAnswer": { + "buttonText": "Correct Answer", + "ariaLabel": "" + }, + "_hideCorrectAnswer": { + "buttonText": "My Answer", + "ariaLabel": "" + }, + "_showFeedback": { + "buttonText": "Show feedback", + "ariaLabel": "" + }, + "remainingAttemptsText": "attempts remaining", + "remainingAttemptText": "final attempt" + }, + "_pageLevelProgress": { + "_isEnabled": true } + } diff --git a/migrations/v7.js b/migrations/v7.js index fa934e1..c0c1be6 100644 --- a/migrations/v7.js +++ b/migrations/v7.js @@ -54,6 +54,47 @@ describe('MCQ - v7.2.1 to v7.3.0', async () => { }); }); +// https://github.com/adaptlearning/adapt-contrib-mcq/commit/4f789793248c5f40529897f5a990cee79758c1b7 +describe('MCQ - v7.3.7 to v7.3.8', async () => { + let MCQs; + whereFromPlugin('MCQ - from v7.3.7', { name: 'adapt-contrib-mcq', version: '<7.3.8' }); + whereContent('MCQ - where MCQ with _feedback', async (content) => { + MCQs = getComponents('mcq').filter(MCQ => _.has(MCQ, '_feedback') && !_.has(MCQ._feedback, 'altTitle')); + return MCQs.length; + }); + mutateContent('MCQ - add altTitle to _feedback', async (content) => { + MCQs.forEach(MCQ => { + MCQ._feedback.altTitle = ''; + }); + return true; + }); + checkContent('MCQ - check _feedback altTitle attribute', async (content) => { + const isValid = MCQs.every(MCQ => _.has(MCQ._feedback, 'altTitle')); + if (!isValid) throw new Error('MCQ - no _feedback altTitle found'); + return true; + }); + updatePlugin('MCQ - update to v7.3.8', { name: 'adapt-contrib-mcq', version: '7.3.8', framework: '>=5.19.1' }); + + testSuccessWhere('mcq components with _feedback', { + fromPlugins: [{ name: 'adapt-contrib-mcq', version: '7.3.7' }], + content: [ + { _id: 'c-100', _component: 'mcq', _feedback: { correct: 'Correct!' } }, + { _id: 'c-105', _component: 'mcq', _feedback: { correct: '' } } + ] + }); + + testStopWhere('incorrect version', { + fromPlugins: [{ name: 'adapt-contrib-mcq', version: '7.3.8' }] + }); + + testStopWhere('no mcq components with _feedback', { + fromPlugins: [{ name: 'adapt-contrib-mcq', version: '7.3.7' }], + content: [ + { _id: 'c-100', _component: 'mcq' } + ] + }); +}); + // https://github.com/adaptlearning/adapt-contrib-mcq/compare/v7.3.11...v7.4.0 describe('MCQ - v7.3.11 to v7.4.0', async () => { let MCQs; @@ -116,7 +157,7 @@ describe('MCQ - v7.4.3 to v7.5.0', async () => { if (!isValid) throw new Error('MCQ - no _canShowCorrectness attribute found'); return true; }); - updatePlugin('MCQ - update to v7.6.0', { name: 'adapt-contrib-mcq', version: '7.5.0', framework: '>=5.19.1' }); + updatePlugin('MCQ - update to v7.5.0', { name: 'adapt-contrib-mcq', version: '7.5.0', framework: '>=5.19.1' }); testSuccessWhere('correct version mcq components', { fromPlugins: [{ name: 'adapt-contrib-mcq', version: '7.4.3' }], @@ -135,3 +176,95 @@ describe('MCQ - v7.4.3 to v7.5.0', async () => { content: [{ _component: 'other' }] }); }); + +// https://github.com/adaptlearning/adapt-contrib-mcq/issues/251 +describe('MCQ - v7.5.0 to @@RELEASE_VERSION', async () => { + let MCQs; + whereFromPlugin('MCQ - from v7.5.0', { name: 'adapt-contrib-mcq', version: '<@@RELEASE_VERSION' }); + whereContent('MCQ - where MCQ with legacy feedback', async (content) => { + MCQs = getComponents('mcq').filter(MCQ => + _.has(MCQ, '_feedback.correct') || + _.has(MCQ, '_feedback._incorrect') || + _.has(MCQ, '_feedback._partlyCorrect') + ); + return MCQs.length; + }); + mutateContent('MCQ - convert _feedback.correct to _feedback._correct object', async (content) => { + MCQs.forEach(MCQ => { + MCQ._feedback._correct = { body: MCQ._feedback.correct }; + _.unset(MCQ._feedback, 'correct'); + }); + return true; + }); + mutateContent('MCQ - convert _feedback._incorrect to _feedback._incorrectFinal/_incorrectNotFinal objects', async (content) => { + MCQs.forEach(MCQ => { + if (!_.has(MCQ, '_feedback._incorrect')) return; + MCQ._feedback._incorrectFinal = { body: MCQ._feedback._incorrect.final ?? '' }; + MCQ._feedback._incorrectNotFinal = { body: MCQ._feedback._incorrect.notFinal ?? '' }; + _.unset(MCQ._feedback, '_incorrect'); + }); + return true; + }); + mutateContent('MCQ - convert _feedback._partlyCorrect to _feedback._partlyCorrectFinal/_partlyCorrectNotFinal objects', async (content) => { + MCQs.forEach(MCQ => { + if (!_.has(MCQ, '_feedback._partlyCorrect')) return; + MCQ._feedback._partlyCorrectFinal = { body: MCQ._feedback._partlyCorrect.final ?? '' }; + MCQ._feedback._partlyCorrectNotFinal = { body: MCQ._feedback._partlyCorrect.notFinal ?? '' }; + _.unset(MCQ._feedback, '_partlyCorrect'); + }); + return true; + }); + checkContent('MCQ - check legacy _feedback keys removed', async (content) => { + const isValid = MCQs.every(MCQ => + !_.has(MCQ, '_feedback.correct') && + !_.has(MCQ, '_feedback._incorrect') && + !_.has(MCQ, '_feedback._partlyCorrect') + ); + if (!isValid) throw new Error('MCQ - legacy _feedback keys still present'); + return true; + }); + checkContent('MCQ - check _feedback._correct.body present', async (content) => { + const isValid = MCQs.every(MCQ => _.has(MCQ, '_feedback._correct.body')); + if (!isValid) throw new Error('MCQ - _feedback._correct.body not found'); + return true; + }); + updatePlugin('MCQ - update to @@RELEASE_VERSION', { name: 'adapt-contrib-mcq', version: '@@RELEASE_VERSION', framework: '>=5.19.1' }); + + testSuccessWhere('mcq components with legacy flat string feedback', { + fromPlugins: [{ name: 'adapt-contrib-mcq', version: '7.5.0' }], + content: [ + { + _id: 'c-100', + _component: 'mcq', + _feedback: { + correct: 'Well done.', + _incorrect: { final: 'Sorry, that is wrong.', notFinal: 'Try again.' }, + _partlyCorrect: { final: 'Nearly!', notFinal: 'Almost there.' } + } + }, + { + _id: 'c-105', + _component: 'mcq', + _feedback: { + correct: 'Correct!', + _incorrect: { final: 'Incorrect.' } + } + } + ] + }); + + testStopWhere('incorrect version', { + fromPlugins: [{ name: 'adapt-contrib-mcq', version: '@@RELEASE_VERSION' }] + }); + + testStopWhere('no mcq components with legacy feedback', { + fromPlugins: [{ name: 'adapt-contrib-mcq', version: '7.5.0' }], + content: [ + { + _id: 'c-100', + _component: 'mcq', + _feedback: { _correct: { body: 'Well done.' } } + } + ] + }); +}); diff --git a/properties.schema b/properties.schema index a13ea33..0b1a45b 100644 --- a/properties.schema +++ b/properties.schema @@ -121,14 +121,95 @@ "validators": [] }, "feedback": { - "type": "string", + "type": "object", "required": false, - "default": "", "title": "Answer-specific feedback", - "inputType": "TextArea", + "inputType": "Object", "validators": [], - "help": "When 'Selectable Items' is set to 1, this can be used to give the user feedback specific to the answer they selected - if incorrect.", - "translatable": true + "help": "When 'Selectable Items' is set to 1, this gives the user feedback specific to the answer they selected - if incorrect. Overrides the component-level _feedback state for this item.", + "properties": { + "title": { + "type": "string", + "required": false, + "default": "", + "title": "Title", + "inputType": "Text", + "validators": [], + "translatable": true + }, + "altTitle": { + "type": "string", + "required": false, + "default": "", + "title": "Alternative Title", + "inputType": "Text", + "validators": [], + "help": "Text read out by screen readers if no visual title is included.", + "translatable": true + }, + "body": { + "type": "string", + "required": false, + "default": "", + "title": "Body", + "inputType": "TextArea", + "validators": [], + "translatable": true + }, + "_classes": { + "type": "string", + "required": false, + "default": "", + "title": "Classes", + "inputType": "Text", + "validators": [] + }, + "_imageAlignment": { + "type": "string", + "required": false, + "default": "right", + "title": "Image Alignment", + "inputType": "Select", + "enum": ["left", "right"], + "validators": [], + "help": "Alignment of the graphic relative to the feedback text. Defaults to \"right\"." + }, + "_graphic": { + "type": "object", + "required": false, + "title": "Graphic", + "inputType": "Object", + "validators": [], + "properties": { + "_src": { + "type": "string", + "required": false, + "default": "", + "title": "Source", + "inputType": "Asset:image", + "validators": [] + }, + "alt": { + "type": "string", + "required": false, + "default": "", + "title": "Alternative Text", + "inputType": "Text", + "validators": [], + "translatable": true + }, + "attribution": { + "type": "string", + "required": false, + "default": "", + "title": "Attribution", + "inputType": "Text", + "validators": [], + "translatable": true + } + } + } + } }, "_score": { "type": "number", @@ -249,69 +330,465 @@ "title": "Feedback Title", "inputType": "Text", "validators": [], - "help": "Leave blank to have the component's title shown instead.", + "help": "Default title shown in the feedback modal header. Per-state objects can override this with their own 'title' field; if a state's 'title' is empty or absent, this value is used as the fallback.", "translatable": true }, - "correct": { + "altTitle": { "type": "string", "required": false, "default": "", - "title": "Correct", - "inputType": "TextArea", + "title": "Alternative Feedback Title", + "inputType": "Text", "validators": [], - "help": "Correct answer feedback for this question", + "help": "Text read out by screen readers if no visual title is included.", "translatable": true }, - "_incorrect": { + "_classes": { + "type": "string", + "required": false, + "default": "", + "title": "Feedback Classes", + "inputType": "Text", + "validators": [], + "help": "CSS class name(s) to be applied to the feedback panel for all states. Separate multiple classes with a space." + }, + "_correct": { "type": "object", "required": false, - "title": "Incorrect Feedback", + "title": "Correct Feedback", + "help": "Feedback shown when the answer is correct.", "properties": { - "final": { + "title": { + "type": "string", + "required": false, + "default": "", + "title": "Title", + "inputType": "Text", + "validators": [], + "translatable": true + }, + "altTitle": { + "type": "string", + "required": false, + "default": "", + "title": "Alternative Title", + "inputType": "Text", + "validators": [], + "help": "Text read out by screen readers if no visual title is included.", + "translatable": true + }, + "body": { "type": "string", "required": false, "default": "", - "title": "Incorrect Final", + "title": "Body", "inputType": "TextArea", "validators": [], - "help": "Incorrect answer feedback for the final attempt", "translatable": true }, - "notFinal": { + "_classes": { + "type": "string", + "required": false, + "default": "", + "title": "Classes", + "inputType": "Text", + "validators": [] + }, + "_imageAlignment": { + "type": "string", + "required": false, + "default": "right", + "title": "Image Alignment", + "inputType": "Select", + "enum": ["left", "right"], + "validators": [], + "help": "Alignment of the graphic relative to the feedback text. Defaults to \"right\"." + }, + "_graphic": { + "type": "object", + "required": false, + "title": "Graphic", + "properties": { + "_src": { + "type": "string", + "required": false, + "default": "", + "title": "Source", + "inputType": "Asset:image", + "validators": [] + }, + "alt": { + "type": "string", + "required": false, + "default": "", + "title": "Alternative Text", + "inputType": "Text", + "validators": [], + "help": "A description of the image for screen readers.", + "translatable": true + }, + "attribution": { + "type": "string", + "required": false, + "default": "", + "title": "Attribution", + "inputType": "Text", + "validators": [], + "translatable": true + } + } + } + } + }, + "_incorrectFinal": { + "type": "object", + "required": false, + "title": "Incorrect Feedback - Final Attempt", + "help": "Feedback shown when the answer is incorrect on the final attempt.", + "properties": { + "title": { + "type": "string", + "required": false, + "default": "", + "title": "Title", + "inputType": "Text", + "validators": [], + "translatable": true + }, + "altTitle": { + "type": "string", + "required": false, + "default": "", + "title": "Alternative Title", + "inputType": "Text", + "validators": [], + "help": "Text read out by screen readers if no visual title is included.", + "translatable": true + }, + "body": { "type": "string", "required": false, "default": "", - "title": "Incorrect Not Final", + "title": "Body", "inputType": "TextArea", "validators": [], - "help": "Incorrect answer feedback for any attempt apart from the last attempt. If you leave this blank, the 'Incorrect Final' feedback will be used instead.", "translatable": true + }, + "_classes": { + "type": "string", + "required": false, + "default": "", + "title": "Classes", + "inputType": "Text", + "validators": [] + }, + "_imageAlignment": { + "type": "string", + "required": false, + "default": "right", + "title": "Image Alignment", + "inputType": "Select", + "enum": ["left", "right"], + "validators": [], + "help": "Alignment of the graphic relative to the feedback text. Defaults to \"right\"." + }, + "_graphic": { + "type": "object", + "required": false, + "title": "Graphic", + "properties": { + "_src": { + "type": "string", + "required": false, + "default": "", + "title": "Source", + "inputType": "Asset:image", + "validators": [] + }, + "alt": { + "type": "string", + "required": false, + "default": "", + "title": "Alternative Text", + "inputType": "Text", + "validators": [], + "help": "A description of the image for screen readers.", + "translatable": true + }, + "attribution": { + "type": "string", + "required": false, + "default": "", + "title": "Attribution", + "inputType": "Text", + "validators": [], + "translatable": true + } + } } } }, - "_partlyCorrect": { + "_incorrectNotFinal": { "type": "object", "required": false, + "title": "Incorrect Feedback - Not Final Attempt", + "help": "Feedback shown when the answer is incorrect and attempts remain. If omitted, 'Incorrect Feedback - Final Attempt' is used instead.", "properties": { - "final": { + "title": { + "type": "string", + "required": false, + "default": "", + "title": "Title", + "inputType": "Text", + "validators": [], + "translatable": true + }, + "altTitle": { + "type": "string", + "required": false, + "default": "", + "title": "Alternative Title", + "inputType": "Text", + "validators": [], + "help": "Text read out by screen readers if no visual title is included.", + "translatable": true + }, + "body": { "type": "string", "required": false, "default": "", - "title": "Partly Correct Final", + "title": "Body", "inputType": "TextArea", "validators": [], - "help": "Partly correct answer feedback for the final attempt. If you leave this blank, the 'Incorrect Final' feedback will be used instead.", "translatable": true }, - "notFinal": { + "_classes": { "type": "string", "required": false, "default": "", - "title": "Partly Correct Not Final", + "title": "Classes", + "inputType": "Text", + "validators": [] + }, + "_imageAlignment": { + "type": "string", + "required": false, + "default": "right", + "title": "Image Alignment", + "inputType": "Select", + "enum": ["left", "right"], + "validators": [], + "help": "Alignment of the graphic relative to the feedback text. Defaults to \"right\"." + }, + "_graphic": { + "type": "object", + "required": false, + "title": "Graphic", + "properties": { + "_src": { + "type": "string", + "required": false, + "default": "", + "title": "Source", + "inputType": "Asset:image", + "validators": [] + }, + "alt": { + "type": "string", + "required": false, + "default": "", + "title": "Alternative Text", + "inputType": "Text", + "validators": [], + "help": "A description of the image for screen readers.", + "translatable": true + }, + "attribution": { + "type": "string", + "required": false, + "default": "", + "title": "Attribution", + "inputType": "Text", + "validators": [], + "translatable": true + } + } + } + } + }, + "_partlyCorrectFinal": { + "type": "object", + "required": false, + "title": "Partly Correct Feedback - Final Attempt", + "help": "Feedback shown when the answer is partly correct on the final attempt. If omitted, 'Incorrect Feedback - Final Attempt' is used instead.", + "properties": { + "title": { + "type": "string", + "required": false, + "default": "", + "title": "Title", + "inputType": "Text", + "validators": [], + "translatable": true + }, + "altTitle": { + "type": "string", + "required": false, + "default": "", + "title": "Alternative Title", + "inputType": "Text", + "validators": [], + "help": "Text read out by screen readers if no visual title is included.", + "translatable": true + }, + "body": { + "type": "string", + "required": false, + "default": "", + "title": "Body", "inputType": "TextArea", "validators": [], - "help": "Partly correct answer feedback for any attempt apart from the last attempt. If you leave this blank, the 'Partly Correct Final' feedback will be used instead.", "translatable": true + }, + "_classes": { + "type": "string", + "required": false, + "default": "", + "title": "Classes", + "inputType": "Text", + "validators": [] + }, + "_imageAlignment": { + "type": "string", + "required": false, + "default": "right", + "title": "Image Alignment", + "inputType": "Select", + "enum": ["left", "right"], + "validators": [], + "help": "Alignment of the graphic relative to the feedback text. Defaults to \"right\"." + }, + "_graphic": { + "type": "object", + "required": false, + "title": "Graphic", + "properties": { + "_src": { + "type": "string", + "required": false, + "default": "", + "title": "Source", + "inputType": "Asset:image", + "validators": [] + }, + "alt": { + "type": "string", + "required": false, + "default": "", + "title": "Alternative Text", + "inputType": "Text", + "validators": [], + "help": "A description of the image for screen readers.", + "translatable": true + }, + "attribution": { + "type": "string", + "required": false, + "default": "", + "title": "Attribution", + "inputType": "Text", + "validators": [], + "translatable": true + } + } + } + } + }, + "_partlyCorrectNotFinal": { + "type": "object", + "required": false, + "title": "Partly Correct Feedback - Not Final Attempt", + "help": "Feedback shown when the answer is partly correct and attempts remain. If omitted, 'Partly Correct Feedback - Final Attempt' is used instead.", + "properties": { + "title": { + "type": "string", + "required": false, + "default": "", + "title": "Title", + "inputType": "Text", + "validators": [], + "translatable": true + }, + "altTitle": { + "type": "string", + "required": false, + "default": "", + "title": "Alternative Title", + "inputType": "Text", + "validators": [], + "help": "Text read out by screen readers if no visual title is included.", + "translatable": true + }, + "body": { + "type": "string", + "required": false, + "default": "", + "title": "Body", + "inputType": "TextArea", + "validators": [], + "translatable": true + }, + "_classes": { + "type": "string", + "required": false, + "default": "", + "title": "Classes", + "inputType": "Text", + "validators": [] + }, + "_imageAlignment": { + "type": "string", + "required": false, + "default": "right", + "title": "Image Alignment", + "inputType": "Select", + "enum": ["left", "right"], + "validators": [], + "help": "Alignment of the graphic relative to the feedback text. Defaults to \"right\"." + }, + "_graphic": { + "type": "object", + "required": false, + "title": "Graphic", + "properties": { + "_src": { + "type": "string", + "required": false, + "default": "", + "title": "Source", + "inputType": "Asset:image", + "validators": [] + }, + "alt": { + "type": "string", + "required": false, + "default": "", + "title": "Alternative Text", + "inputType": "Text", + "validators": [], + "help": "A description of the image for screen readers.", + "translatable": true + }, + "attribution": { + "type": "string", + "required": false, + "default": "", + "title": "Attribution", + "inputType": "Text", + "validators": [], + "translatable": true + } + } } } } diff --git a/schema/component.schema.json b/schema/component.schema.json index 46756c7..a916172 100644 --- a/schema/component.schema.json +++ b/schema/component.schema.json @@ -78,14 +78,90 @@ "default": false }, "feedback": { - "type": "string", + "type": "object", "title": "Answer-specific feedback", - "description": "When 'Selectable items' is set to 1, this can be used to give the user feedback specific to the answer they selected - if incorrect", - "default": "", - "_adapt": { - "translatable": true - }, - "_backboneForms": "TextArea" + "description": "When 'Selectable items' is set to 1, this gives the user feedback specific to the answer they selected - if incorrect. Overrides the component-level _feedback state for this item.", + "default": {}, + "properties": { + "title": { + "type": "string", + "title": "Title", + "description": "Feedback title for this item. Overrides the component-level _feedback.title.", + "default": "", + "_adapt": { + "translatable": true + } + }, + "altTitle": { + "type": "string", + "title": "Alternative title", + "description": "Alternative title read by screen readers if no visual title is included.", + "default": "", + "_adapt": { + "translatable": true + } + }, + "body": { + "type": "string", + "title": "Body", + "description": "Feedback body text for this item.", + "default": "", + "_adapt": { + "translatable": true + }, + "_backboneForms": "TextArea" + }, + "_classes": { + "type": "string", + "title": "Classes", + "description": "CSS class name(s) for this item's feedback panel.", + "default": "" + }, + "_imageAlignment": { + "type": "string", + "title": "Image alignment", + "description": "Alignment of the graphic relative to the feedback text. Defaults to 'right'.", + "default": "right", + "enum": [ + "left", + "right" + ] + }, + "_graphic": { + "type": "object", + "title": "Graphic", + "default": {}, + "properties": { + "_src": { + "type": "string", + "title": "Source", + "default": "", + "_backboneForms": { + "type": "Asset", + "media": "image" + } + }, + "alt": { + "type": "string", + "title": "Alternative text", + "description": "A description of the image for screen readers", + "default": "", + "_adapt": { + "translatable": true + } + }, + "attribution": { + "type": "string", + "title": "Attribution", + "description": "Text to be displayed as an attribution", + "default": "", + "_adapt": { + "translatable": true + } + } + } + } + } }, "_score": { "type": "number", @@ -161,12 +237,13 @@ "_feedback": { "type": "object", "title": "Feedback", + "description": "Feedback configuration for the component. Uses per-state objects (_correct, _incorrectFinal, etc.) to support body text, title overrides, CSS classes, and an optional graphic per state.", "default": {}, "properties": { "title": { "type": "string", "title": "Title", - "description": "Title text for the feedback", + "description": "Default title shown in the feedback modal header. Per-state objects can override this with their own 'title' field; if a state's 'title' is empty or absent, this value is used as the fallback.", "default": "", "_adapt": { "translatable": true @@ -181,67 +258,439 @@ "translatable": true } }, - "correct": { + "_classes": { "type": "string", + "title": "Classes", + "description": "CSS class name(s) to be applied to the feedback panel for all states. Separate multiple classes with a space.", + "default": "" + }, + "_correct": { + "type": "object", "title": "Correct feedback", - "description": "Correct answer feedback for this question", - "default": "", - "_adapt": { - "translatable": true - }, - "_backboneForms": "TextArea" + "description": "Feedback shown when the answer is correct.", + "default": {}, + "properties": { + "title": { + "type": "string", + "title": "Title", + "description": "Feedback title for this state. Overrides the top-level _feedback.title for this state only.", + "default": "", + "_adapt": { + "translatable": true + } + }, + "altTitle": { + "type": "string", + "title": "Alternative title", + "description": "Alternative title read by screen readers if no visual title is included. Overrides the top-level _feedback.altTitle for this state only.", + "default": "", + "_adapt": { + "translatable": true + } + }, + "body": { + "type": "string", + "title": "Body", + "description": "Feedback body text for this state.", + "default": "", + "_adapt": { + "translatable": true + }, + "_backboneForms": "TextArea" + }, + "_classes": { + "type": "string", + "title": "Classes", + "description": "CSS class name(s) for this state only. Overrides the top-level _feedback._classes for this state.", + "default": "" + }, + "_imageAlignment": { + "type": "string", + "title": "Image alignment", + "description": "Alignment of the graphic relative to the feedback text. Defaults to 'right'.", + "default": "right", + "enum": [ + "left", + "right" + ] + }, + "_graphic": { + "type": "object", + "title": "Graphic", + "default": {}, + "properties": { + "_src": { + "type": "string", + "title": "Source", + "default": "", + "_backboneForms": { + "type": "Asset", + "media": "image" + } + }, + "alt": { + "type": "string", + "title": "Alternative text", + "description": "A description of the image for screen readers", + "default": "", + "_adapt": { + "translatable": true + } + }, + "attribution": { + "type": "string", + "title": "Attribution", + "description": "Text to be displayed as an attribution", + "default": "", + "_adapt": { + "translatable": true + } + } + } + } + } }, - "_incorrect": { + "_incorrectFinal": { "type": "object", - "title": "Incorrect", + "title": "Incorrect feedback (final attempt)", + "description": "Feedback shown when the answer is incorrect on the final attempt.", "default": {}, "properties": { - "final": { + "title": { + "type": "string", + "title": "Title", + "description": "Feedback title for this state. Overrides the top-level _feedback.title for this state only.", + "default": "", + "_adapt": { + "translatable": true + } + }, + "altTitle": { + "type": "string", + "title": "Alternative title", + "description": "Alternative title read by screen readers if no visual title is included. Overrides the top-level _feedback.altTitle for this state only.", + "default": "", + "_adapt": { + "translatable": true + } + }, + "body": { "type": "string", - "title": "Default incorrect feedback", - "description": "Incorrect answer feedback for the final attempt", + "title": "Body", + "description": "Feedback body text for this state.", "default": "", "_adapt": { "translatable": true }, "_backboneForms": "TextArea" }, - "notFinal": { + "_classes": { + "type": "string", + "title": "Classes", + "description": "CSS class name(s) for this state only. Overrides the top-level _feedback._classes for this state.", + "default": "" + }, + "_imageAlignment": { + "type": "string", + "title": "Image alignment", + "description": "Alignment of the graphic relative to the feedback text. Defaults to 'right'.", + "default": "right", + "enum": [ + "left", + "right" + ] + }, + "_graphic": { + "type": "object", + "title": "Graphic", + "default": {}, + "properties": { + "_src": { + "type": "string", + "title": "Source", + "default": "", + "_backboneForms": { + "type": "Asset", + "media": "image" + } + }, + "alt": { + "type": "string", + "title": "Alternative text", + "description": "A description of the image for screen readers", + "default": "", + "_adapt": { + "translatable": true + } + }, + "attribution": { + "type": "string", + "title": "Attribution", + "description": "Text to be displayed as an attribution", + "default": "", + "_adapt": { + "translatable": true + } + } + } + } + } + }, + "_incorrectNotFinal": { + "type": "object", + "title": "Incorrect feedback (not final attempt)", + "description": "Feedback shown when the answer is incorrect and attempts remain. Falls back to _incorrectFinal if omitted.", + "default": {}, + "properties": { + "title": { + "type": "string", + "title": "Title", + "description": "Feedback title for this state. Overrides the top-level _feedback.title for this state only.", + "default": "", + "_adapt": { + "translatable": true + } + }, + "altTitle": { + "type": "string", + "title": "Alternative title", + "description": "Alternative title read by screen readers if no visual title is included. Overrides the top-level _feedback.altTitle for this state only.", + "default": "", + "_adapt": { + "translatable": true + } + }, + "body": { "type": "string", - "title": "Retry feedback", - "description": "Incorrect answer feedback for any attempt apart from the last attempt. If you leave this blank, the default incorrect feedback will be used instead", + "title": "Body", + "description": "Feedback body text for this state.", "default": "", "_adapt": { "translatable": true }, "_backboneForms": "TextArea" + }, + "_classes": { + "type": "string", + "title": "Classes", + "description": "CSS class name(s) for this state only. Overrides the top-level _feedback._classes for this state.", + "default": "" + }, + "_imageAlignment": { + "type": "string", + "title": "Image alignment", + "description": "Alignment of the graphic relative to the feedback text. Defaults to 'right'.", + "default": "right", + "enum": [ + "left", + "right" + ] + }, + "_graphic": { + "type": "object", + "title": "Graphic", + "default": {}, + "properties": { + "_src": { + "type": "string", + "title": "Source", + "default": "", + "_backboneForms": { + "type": "Asset", + "media": "image" + } + }, + "alt": { + "type": "string", + "title": "Alternative text", + "description": "A description of the image for screen readers", + "default": "", + "_adapt": { + "translatable": true + } + }, + "attribution": { + "type": "string", + "title": "Attribution", + "description": "Text to be displayed as an attribution", + "default": "", + "_adapt": { + "translatable": true + } + } + } } } }, - "_partlyCorrect": { + "_partlyCorrectFinal": { "type": "object", - "title": "Partly correct", + "title": "Partly correct feedback (final attempt)", + "description": "Feedback shown when the answer is partly correct on the final attempt. Falls back to _incorrectFinal if omitted.", "default": {}, "properties": { - "final": { + "title": { + "type": "string", + "title": "Title", + "description": "Feedback title for this state. Overrides the top-level _feedback.title for this state only.", + "default": "", + "_adapt": { + "translatable": true + } + }, + "altTitle": { + "type": "string", + "title": "Alternative title", + "description": "Alternative title read by screen readers if no visual title is included. Overrides the top-level _feedback.altTitle for this state only.", + "default": "", + "_adapt": { + "translatable": true + } + }, + "body": { "type": "string", - "title": "Default partly correct feedback", - "description": "Partly correct answer feedback for the final attempt. If you leave this blank, the default incorrect feedback will be used instead", + "title": "Body", + "description": "Feedback body text for this state.", "default": "", "_adapt": { "translatable": true }, "_backboneForms": "TextArea" }, - "notFinal": { + "_classes": { + "type": "string", + "title": "Classes", + "description": "CSS class name(s) for this state only. Overrides the top-level _feedback._classes for this state.", + "default": "" + }, + "_imageAlignment": { + "type": "string", + "title": "Image alignment", + "description": "Alignment of the graphic relative to the feedback text. Defaults to 'right'.", + "default": "right", + "enum": [ + "left", + "right" + ] + }, + "_graphic": { + "type": "object", + "title": "Graphic", + "default": {}, + "properties": { + "_src": { + "type": "string", + "title": "Source", + "default": "", + "_backboneForms": { + "type": "Asset", + "media": "image" + } + }, + "alt": { + "type": "string", + "title": "Alternative text", + "description": "A description of the image for screen readers", + "default": "", + "_adapt": { + "translatable": true + } + }, + "attribution": { + "type": "string", + "title": "Attribution", + "description": "Text to be displayed as an attribution", + "default": "", + "_adapt": { + "translatable": true + } + } + } + } + } + }, + "_partlyCorrectNotFinal": { + "type": "object", + "title": "Partly correct feedback (not final attempt)", + "description": "Feedback shown when the answer is partly correct and attempts remain. Falls back to _partlyCorrectFinal, then _incorrectFinal if omitted.", + "default": {}, + "properties": { + "title": { + "type": "string", + "title": "Title", + "description": "Feedback title for this state. Overrides the top-level _feedback.title for this state only.", + "default": "", + "_adapt": { + "translatable": true + } + }, + "altTitle": { + "type": "string", + "title": "Alternative title", + "description": "Alternative title read by screen readers if no visual title is included. Overrides the top-level _feedback.altTitle for this state only.", + "default": "", + "_adapt": { + "translatable": true + } + }, + "body": { "type": "string", - "title": "Retry feedback", - "description": "Partly correct answer feedback for any attempt apart from the last attempt. If you leave this blank, the default partly correct feedback will be used instead", + "title": "Body", + "description": "Feedback body text for this state.", "default": "", "_adapt": { "translatable": true }, "_backboneForms": "TextArea" + }, + "_classes": { + "type": "string", + "title": "Classes", + "description": "CSS class name(s) for this state only. Overrides the top-level _feedback._classes for this state.", + "default": "" + }, + "_imageAlignment": { + "type": "string", + "title": "Image alignment", + "description": "Alignment of the graphic relative to the feedback text. Defaults to 'right'.", + "default": "right", + "enum": [ + "left", + "right" + ] + }, + "_graphic": { + "type": "object", + "title": "Graphic", + "default": {}, + "properties": { + "_src": { + "type": "string", + "title": "Source", + "default": "", + "_backboneForms": { + "type": "Asset", + "media": "image" + } + }, + "alt": { + "type": "string", + "title": "Alternative text", + "description": "A description of the image for screen readers", + "default": "", + "_adapt": { + "translatable": true + } + }, + "attribution": { + "type": "string", + "title": "Attribution", + "description": "Text to be displayed as an attribution", + "default": "", + "_adapt": { + "translatable": true + } + } + } } } }