Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Creating mobile question types: Difference between revisions

From MoodleDocs
No edit summary
(16 intermediate revisions by the same user not shown)
Line 3: Line 3:


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
Line 38: Line 44:


==== html template ====
==== html template ====
Your question type will need an html/ionic markup template file that controls how it is displayed at runtime. You can see a real world examples of this file at  
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
Line 49: Line 59:
Create a file named mobile/addon-qtype-YOURQTYPENAME.html
Create a file named mobile/addon-qtype-YOURQTYPENAME.html


<code>
<code xml>
<section class="list qtype-YOURQTYPENAME-container qtype-YOURQTYPENAME" 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 class="addon-qtype-YOURQTYPENAME-container qtext">
     <ion-item text-wrap class="addon-qtype-YOURQTYPENAME-container qtext">
Line 57: Line 67:
</section>
</section>
</code>
</code>
The reference to questionRendered() will call the method of that name in the mobile/mobile.js file.


==== classes/output/mobile.php ====
==== 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
This file connects PHP with the html/ionic markup template and the mobile.js javascript which contains most of the runtime logic
<code>
<code php>
namespace qtype_YOURQTYPENAME\output;
namespace qtype_YOURQTYPENAME\output;


Line 83: Line 95:
    
    
</Code>
</Code>
==== mobile/mobile.js ====
==== mobile/mobile.js ====
<code>
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 that = this;
var result = {
var result = {
     componentInit: function() {
     componentInit: function() {
        /**
        * If the question is in a readonly state, e.g. after being
        * answered or in the review page then stop any further
        * selections.
        *
        * @param {NodeList} draggables
        * @param {MouseEvent} event
        * @return {string} value of target
        **/
         if (!this.question) {
         if (!this.question) {
             console.warn('Aborting because of no question received.');
             console.warn('Aborting because of no question received.');
Line 108: Line 118:


         // Replace Moodle's correct/incorrect and feedback classes with our own.
         // 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.replaceCorrectnessClasses(div);
         this.CoreQuestionHelperProvider.replaceFeedbackClasses(div);
         this.CoreQuestionHelperProvider.replaceFeedbackClasses(div);
Line 122: Line 133:
             this.question.feedbackHTML = true;
             this.question.feedbackHTML = true;
         }
         }
       
        this.question.text = this.CoreDomUtilsProvider.getContentsOfElement(div, '.qtext');


         if (typeof this.question.text == 'undefined') {
         if (typeof this.question.text == 'undefined') {
             this.logger.warn('Aborting because of an error parsing question.', this.question.name);
             this.logger.warn('Aborting because of an error parsing question.', this.question.name);
             return this.CoreQuestionHelperProvider.showComponentError(this.onAbort);
             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.
         // Wait for the DOM to be rendered.
         setTimeout(() => {
         setTimeout(() => {
            //put stuff here that will be pulled from the rendered question
         });
         });
         return true;
         return true;
Line 139: Line 158:


==== mobile/styles_app.css ====
==== mobile/styles_app.css ====
This assumes you have class="list qtype-YOURQTYPENAME-container in your html template. Changes will be updated when you increment the value
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
of styles[version] in  db/mobile.php
 
<code css>
.qtype-YOURQTYPENAME .yourclassname {
.qtype-YOURQTYPENAME .yourclassname {
     attributename: attributevalue;
     attributename: attributevalue;
}
}
</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

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

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;

}