Creating mobile question types: Difference between revisions
Marcus Green (talk | contribs) No edit summary |
Marcus Green (talk | contribs) No edit summary |
||
(28 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
An empty template for creating quiz question types is available from here | An empty template for creating quiz question types with mobile support is available from here | ||
https://github.com/marcusgreen/moodle-qtype_TEMPLATE | https://github.com/marcusgreen/moodle-qtype_TEMPLATE | ||
There was a presentation on modifying questions type to support the app at the November 2018 developers meeting. You can find a recording of this here | |||
https://moodle.zoom.us/recording/play/mtX7qcu_G9HnEIxfEuxjMVCX7PmZqAG7128XnAWUkLfvVLusBS07tIyJ5U-pv1Sm?continueMode=true | |||
The associated slide show is here. https://docs.google.com/presentation/d/1jUyfULul_g6hh_3s-wISXIVphv_RKqgpHuxMHZ0F800/edit#slide=id.p | |||
The files that need to be modified/added are | The files that need to be modified/added are | ||
==== db/mobile.php ==== | ==== db/mobile.php ==== | ||
Line 44: | Line 42: | ||
]; | ]; | ||
</code> | </code> | ||
Your question type will need an html template file that controls how it is displayed at runtime. | |||
==== html template ==== | |||
Your question type will need an html/ionic markup template file that controls how it is displayed at runtime. | |||
Ionic markup is based on the Angular template syntax which you can read about here | |||
https://angular.io/guide/template-syntax | |||
You can see a real world examples of question type templates at | |||
https://github.com/Beedell/moodle-qtype_oumultiresponse/blob/wip1/mobile/oumr.html | https://github.com/Beedell/moodle-qtype_oumultiresponse/blob/wip1/mobile/oumr.html | ||
and | |||
https://github.com/marcusgreen/moodle-qtype_gapfill/blob/master/mobile/addon-qtype-gapfill.html | |||
(oumr.html is probably a more sensible naming convetion) | |||
Create a file named mobile/addon-qtype-YOURQTYPENAME.html | Create a file named mobile/addon-qtype-YOURQTYPENAME.html | ||
<code> | <code xml> | ||
<section ion-list *ngIf="question.text || question.text === ''"> | <section class="list qtype-YOURQTYPENAME-container qtype-YOURQTYPENAME" ion-list *ngIf="question.text || question.text === ''"> | ||
<ion-item text-wrap> | <ion-item text-wrap class="addon-qtype-YOURQTYPENAME-container qtext"> | ||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p> | <p><core-format-text [component]="component" [componentId]="componentId" | ||
[text]="question.text" (afterRender)="questionRendered()"></core-format-text></p> | |||
</ion-item> | </ion-item> | ||
</section> | </section> | ||
</code> | |||
The reference to questionRendered() will call the method of that name in the mobile/mobile.js file. | |||
==== classes/output/mobile.php ==== | |||
This file connects PHP with the html/ionic markup template and the mobile.js javascript which contains most of the runtime logic | |||
<code php> | |||
namespace qtype_YOURQTYPENAME\output; | |||
defined('MOODLE_INTERNAL') || die(); | |||
class mobile { | |||
class mobile { | |||
public static function mobile_get_YOURQTYPENAME() { | |||
global $CFG; | |||
return [ | |||
'templates' => [ | |||
[ | |||
'id' => 'main', | |||
'html' => file_get_contents($CFG->dirroot .'/question/type/YOURQTYPENAME/mobile/addon-qtype-YOURQTYPENAME.html') | |||
] | |||
], | |||
'javascript' => file_get_contents($CFG->dirroot . '/question/type/YOURQTYPENAME/mobile/mobile.js') | |||
]; | |||
} | |||
} | |||
</Code> | |||
==== mobile/mobile.js ==== | |||
This is where most of the logic is placed such as changing css classes. | |||
You can see real world implementations of this file at | |||
https://github.com/Beedell/moodle-qtype_oumultiresponse/blob/wip1/mobile/oumr.js | |||
and | |||
https://github.com/marcusgreen/moodle-qtype_gapfill/blob/master/mobile/mobile.js | |||
<code js> | |||
var that = this; | |||
var result = { | |||
componentInit: function() { | |||
if (!this.question) { | |||
console.warn('Aborting because of no question received.'); | |||
return that.CoreQuestionHelperProvider.showComponentError(that.onAbort); | |||
} | |||
const div = document.createElement('div'); | |||
div.innerHTML = this.question.html; | |||
// Get question questiontext. | |||
const questiontext = div.querySelector('.qtext'); | |||
// Replace Moodle's correct/incorrect and feedback classes with our own. | |||
// Only do this if you want to use the standard classes | |||
this.CoreQuestionHelperProvider.replaceCorrectnessClasses(div); | |||
this.CoreQuestionHelperProvider.replaceFeedbackClasses(div); | |||
// Treat the correct/incorrect icons. | |||
this.CoreQuestionHelperProvider.treatCorrectnessIcons(div); | |||
if (div.querySelector('.readonly') !== null) { | |||
this.question.readonly = true; | |||
} | |||
if (div.querySelector('.feedback') !== null) { | |||
this.question.feedback = div.querySelector('.feedback'); | |||
this.question.feedbackHTML = true; | |||
} | |||
this.question.text = this.CoreDomUtilsProvider.getContentsOfElement(div, '.qtext'); | |||
if (typeof this.question.text == 'undefined') { | |||
this.logger.warn('Aborting because of an error parsing question.', this.question.name); | |||
return this.CoreQuestionHelperProvider.showComponentError(this.onAbort); | |||
} | |||
// Called by the reference in *.html to | |||
// (afterRender)="questionRendered() | |||
this.questionRendered = function questionRendered() { | |||
//do stuff that needs the question rendered before it can run. | |||
} | |||
// Wait for the DOM to be rendered. | |||
setTimeout(() => { | |||
//put stuff here that will be pulled from the rendered question | |||
}); | |||
return true; | |||
} | |||
}; | |||
result; | |||
</code> | |||
==== mobile/styles_app.css ==== | |||
This assumes you have class="qtype-YOURQTYPENAME-container in your html template. Changes will be updated when you increment the value | |||
of styles[version] in db/mobile.php | |||
<code css> | |||
.qtype-YOURQTYPENAME .yourclassname { | |||
attributename: attributevalue; | |||
} | |||
</code> | </code> |
Revision as of 10:54, 11 January 2019
An empty template for creating quiz question types with mobile support is available from here
https://github.com/marcusgreen/moodle-qtype_TEMPLATE
There was a presentation on modifying questions type to support the app at the November 2018 developers meeting. You can find a recording of this here
The associated slide show is here. https://docs.google.com/presentation/d/1jUyfULul_g6hh_3s-wISXIVphv_RKqgpHuxMHZ0F800/edit#slide=id.p
The files that need to be modified/added are
db/mobile.php
defined('MOODLE_INTERNAL') || die();
$addons = [
"qtype_YOURQTYPENAME" => [
"handlers" => [ // Different places where the add-on will display content.
'YOURQTYPENAME' => [ // Handler unique name (can be anything).
'displaydata' => [
'title' => 'YOURQTYPENAME question',
'icon' => '/question/type/YOURQTYPENAME/pix/icon.gif',
'class' => ,
],
'delegate' => 'CoreQuestionDelegate', // Delegate (where to display the link to the add-on).
'method' => 'mobile_get_YOURQTYPENAME',
'offlinefunctions' => [
'mobile_get_YOURQTYPENAME' => [],// function in classes/output/mobile.php
], // Function needs caching for offline.
'styles' => [
'url' => '/question/type/YOURQTYPENAME/mobile/styles_app.css',
'version' => '1.00'
]
]
],
'lang' => [
['pluginname', 'qtype_YOURQTYPENAME'], // matching value in lang/en/qtype_YOURQTYPENAME
],
]
];
html template
Your question type will need an html/ionic markup template file that controls how it is displayed at runtime. Ionic markup is based on the Angular template syntax which you can read about here https://angular.io/guide/template-syntax
You can see a real world examples of question type templates at
https://github.com/Beedell/moodle-qtype_oumultiresponse/blob/wip1/mobile/oumr.html and
https://github.com/marcusgreen/moodle-qtype_gapfill/blob/master/mobile/addon-qtype-gapfill.html
(oumr.html is probably a more sensible naming convetion)
Create a file named mobile/addon-qtype-YOURQTYPENAME.html
<section class="list qtype-YOURQTYPENAME-container qtype-YOURQTYPENAME" ion-list *ngIf="question.text || question.text === ">
<ion-item text-wrap class="addon-qtype-YOURQTYPENAME-container qtext">
<core-format-text [component]="component" [componentId]="componentId"
[text]="question.text" (afterRender)="questionRendered()"></core-format-text>
</ion-item>
</section>
The reference to questionRendered() will call the method of that name in the mobile/mobile.js file.
classes/output/mobile.php
This file connects PHP with the html/ionic markup template and the mobile.js javascript which contains most of the runtime logic
namespace qtype_YOURQTYPENAME\output;
defined('MOODLE_INTERNAL') || die();
class mobile {
class mobile {
public static function mobile_get_YOURQTYPENAME() {
global $CFG;
return [
'templates' => [
[
'id' => 'main',
'html' => file_get_contents($CFG->dirroot .'/question/type/YOURQTYPENAME/mobile/addon-qtype-YOURQTYPENAME.html')
]
],
'javascript' => file_get_contents($CFG->dirroot . '/question/type/YOURQTYPENAME/mobile/mobile.js')
];
}
}
mobile/mobile.js
This is where most of the logic is placed such as changing css classes. You can see real world implementations of this file at https://github.com/Beedell/moodle-qtype_oumultiresponse/blob/wip1/mobile/oumr.js
and
https://github.com/marcusgreen/moodle-qtype_gapfill/blob/master/mobile/mobile.js
var that = this;
var result = {
componentInit: function() {
if (!this.question) {
console.warn('Aborting because of no question received.');
return that.CoreQuestionHelperProvider.showComponentError(that.onAbort);
}
const div = document.createElement('div');
div.innerHTML = this.question.html;
// Get question questiontext.
const questiontext = div.querySelector('.qtext');
// Replace Moodle's correct/incorrect and feedback classes with our own.
// Only do this if you want to use the standard classes
this.CoreQuestionHelperProvider.replaceCorrectnessClasses(div);
this.CoreQuestionHelperProvider.replaceFeedbackClasses(div);
// Treat the correct/incorrect icons.
this.CoreQuestionHelperProvider.treatCorrectnessIcons(div);
if (div.querySelector('.readonly') !== null) {
this.question.readonly = true;
}
if (div.querySelector('.feedback') !== null) {
this.question.feedback = div.querySelector('.feedback');
this.question.feedbackHTML = true;
}
this.question.text = this.CoreDomUtilsProvider.getContentsOfElement(div, '.qtext');
if (typeof this.question.text == 'undefined') {
this.logger.warn('Aborting because of an error parsing question.', this.question.name);
return this.CoreQuestionHelperProvider.showComponentError(this.onAbort);
}
// Called by the reference in *.html to
// (afterRender)="questionRendered()
this.questionRendered = function questionRendered() {
//do stuff that needs the question rendered before it can run.
}
// Wait for the DOM to be rendered.
setTimeout(() => {
//put stuff here that will be pulled from the rendered question
});
return true;
}
};
result;
mobile/styles_app.css
This assumes you have class="qtype-YOURQTYPENAME-container in your html template. Changes will be updated when you increment the value
of styles[version] in db/mobile.php
.qtype-YOURQTYPENAME .yourclassname {
attributename: attributevalue;
}