Merge pull request #4024 from dpalou/MOBILE-4457
MOBILE-4457 question: Support ordering question type
This commit is contained in:
		
						commit
						497030595e
					
				@ -1147,6 +1147,7 @@
 | 
				
			|||||||
  "addon.privatefiles.sitefiles": "moodle",
 | 
					  "addon.privatefiles.sitefiles": "moodle",
 | 
				
			||||||
  "addon.qtype_essay.maxwordlimitboundary": "qtype_essay",
 | 
					  "addon.qtype_essay.maxwordlimitboundary": "qtype_essay",
 | 
				
			||||||
  "addon.qtype_essay.minwordlimitboundary": "qtype_essay",
 | 
					  "addon.qtype_essay.minwordlimitboundary": "qtype_essay",
 | 
				
			||||||
 | 
					  "addon.qtype_ordering.moved": "qtype_ordering",
 | 
				
			||||||
  "addon.report_insights.actionsaved": "report_insights",
 | 
					  "addon.report_insights.actionsaved": "report_insights",
 | 
				
			||||||
  "addon.report_insights.fixedack": "analytics",
 | 
					  "addon.report_insights.fixedack": "analytics",
 | 
				
			||||||
  "addon.report_insights.incorrectlyflagged": "analytics",
 | 
					  "addon.report_insights.incorrectlyflagged": "analytics",
 | 
				
			||||||
@ -2247,6 +2248,8 @@
 | 
				
			|||||||
  "core.mod_workshop": "workshop/pluginname",
 | 
					  "core.mod_workshop": "workshop/pluginname",
 | 
				
			||||||
  "core.moduleintro": "moodle",
 | 
					  "core.moduleintro": "moodle",
 | 
				
			||||||
  "core.more": "moodle/moremenu",
 | 
					  "core.more": "moodle/moremenu",
 | 
				
			||||||
 | 
					  "core.movedown": "moodle",
 | 
				
			||||||
 | 
					  "core.moveup": "moodle",
 | 
				
			||||||
  "core.mygroups": "group",
 | 
					  "core.mygroups": "group",
 | 
				
			||||||
  "core.name": "moodle",
 | 
					  "core.name": "moodle",
 | 
				
			||||||
  "core.needhelp": "local_moodlemobileapp",
 | 
					  "core.needhelp": "local_moodlemobileapp",
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					@if (question && (question.text || question.text === '')) {
 | 
				
			||||||
 | 
					<div>
 | 
				
			||||||
 | 
					    <ion-item class="ion-text-wrap">
 | 
				
			||||||
 | 
					        <ion-label>
 | 
				
			||||||
 | 
					            <core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
 | 
				
			||||||
 | 
					                [contextInstanceId]="contextInstanceId" [courseId]="courseId" />
 | 
				
			||||||
 | 
					        </ion-label>
 | 
				
			||||||
 | 
					    </ion-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @if (a11yAnnouncement) {
 | 
				
			||||||
 | 
					    <div aria-live="polite" class="sr-only">{{ a11yAnnouncement }}</div>
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <ion-reorder-group class="{{numberingClass}}" [disabled]="dragDisabled" (ionItemReorder)="moveItem($event.detail)">
 | 
				
			||||||
 | 
					        @for (item of question.items; track item.id) {
 | 
				
			||||||
 | 
					        <ion-card id="{{item.id}}" class="core-question-answer-{{item.correctClass}}">
 | 
				
			||||||
 | 
					            <ion-item class="ion-text-wrap">
 | 
				
			||||||
 | 
					                <ion-reorder slot="start" aria-hidden="true" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                @if (dragDisabled) {
 | 
				
			||||||
 | 
					                @if (item.correctClass === 'correct') {
 | 
				
			||||||
 | 
					                <ion-icon name="fas-check" slot="start" />
 | 
				
			||||||
 | 
					                } @else if (item.correctClass === 'incorrect') {
 | 
				
			||||||
 | 
					                <ion-icon name="fas-xmark" slot="start" />
 | 
				
			||||||
 | 
					                } @else if (item.correctClass.startsWith('partial')) {
 | 
				
			||||||
 | 
					                <ion-icon name="far-square-check" slot="start" />
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <ion-label id="{{item.id}}-text">
 | 
				
			||||||
 | 
					                    <core-format-text [component]="component" [componentId]="componentId" [text]="item.content"
 | 
				
			||||||
 | 
					                        [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" />
 | 
				
			||||||
 | 
					                </ion-label>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                @if (!dragDisabled) {
 | 
				
			||||||
 | 
					                <div slot="end" class="flex-row">
 | 
				
			||||||
 | 
					                    <ion-button fill="clear" (click)="moveItemByClick($event, false, item.id)" data-action="move-backward"
 | 
				
			||||||
 | 
					                        [attr.aria-label]="'core.moveup' | translate" [attr.aria-description]="item.contentText">
 | 
				
			||||||
 | 
					                        <ion-icon slot="icon-only" name="fas-chevron-up" aria-hidden="true" />
 | 
				
			||||||
 | 
					                    </ion-button>
 | 
				
			||||||
 | 
					                    <ion-button fill="clear" (click)="moveItemByClick($event, true, item.id)" data-action="move-forward"
 | 
				
			||||||
 | 
					                        [attr.aria-label]="'core.movedown' | translate" [attr.aria-description]="item.contentText">
 | 
				
			||||||
 | 
					                        <ion-icon slot="icon-only" name="fas-chevron-down" aria-hidden="true" />
 | 
				
			||||||
 | 
					                    </ion-button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            </ion-item>
 | 
				
			||||||
 | 
					        </ion-card>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    </ion-reorder-group>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Create a hidden input to hold the value. -->
 | 
				
			||||||
 | 
					    <input type="hidden" [ngModel]="responseInput.value" [attr.name]="responseInput.name">
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										81
									
								
								src/addons/qtype/ordering/component/ordering.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/addons/qtype/ordering/component/ordering.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					@use "theme/globals" as *;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:host {
 | 
				
			||||||
 | 
					    ion-reorder-group .item {
 | 
				
			||||||
 | 
					        ion-label {
 | 
				
			||||||
 | 
					            display: list-item;
 | 
				
			||||||
 | 
					            list-style-position: inside;
 | 
				
			||||||
 | 
					            list-style-type: none;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .core-correct-icon {
 | 
				
			||||||
 | 
					            @include margin-horizontal(0px, 8px);
 | 
				
			||||||
 | 
					            vertical-align: middle;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ion-reorder-group.numbering123 .item ion-label {
 | 
				
			||||||
 | 
					        list-style-type: decimal;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ion-reorder-group.numberingabc .item ion-label {
 | 
				
			||||||
 | 
					        list-style-type: lower-alpha;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ion-reorder-group.numberingABCD .item ion-label {
 | 
				
			||||||
 | 
					        list-style-type: upper-alpha;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ion-reorder-group.numberingiii .item ion-label {
 | 
				
			||||||
 | 
					        list-style-type: lower-roman;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ion-reorder-group.numberingIIII .item ion-label {
 | 
				
			||||||
 | 
					        list-style-type: upper-roman;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ion-reorder-group ion-card {
 | 
				
			||||||
 | 
					        contain: layout paint; // This is needed to make list-style to work properly (otherwise all items were 1/a/i).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:first-of-type [data-action="move-backward"],
 | 
				
			||||||
 | 
					        &:last-of-type [data-action="move-forward"] {
 | 
				
			||||||
 | 
					            visibility: hidden;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &.core-question-answer-correct {
 | 
				
			||||||
 | 
					            --border-color: var(--core-question-correct-color);
 | 
				
			||||||
 | 
					            .item {
 | 
				
			||||||
 | 
					                --color: var(--core-question-correct-color);
 | 
				
			||||||
 | 
					                --background: var(--core-question-correct-color-bg);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &.core-question-answer-incorrect {
 | 
				
			||||||
 | 
					            --border-color: var(--core-question-incorrect-color);
 | 
				
			||||||
 | 
					            .item {
 | 
				
			||||||
 | 
					                --color: var(--core-question-incorrect-color);
 | 
				
			||||||
 | 
					                --background: var(--core-question-incorrect-color-bg);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &.core-question-answer-partial66 {
 | 
				
			||||||
 | 
					            --border-color: #ff9900;
 | 
				
			||||||
 | 
					            .item {
 | 
				
			||||||
 | 
					                --color: var(--core-question-correct-color);
 | 
				
			||||||
 | 
					                --background: var(--core-question-correct-color-bg);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &.core-question-answer-partial33 {
 | 
				
			||||||
 | 
					            --border-color: #ff9900;
 | 
				
			||||||
 | 
					            .item {
 | 
				
			||||||
 | 
					                --color: var(--core-question-feedback-color);
 | 
				
			||||||
 | 
					                --background: var(--core-question-feedback-color-bg);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &.core-question-answer-partial00 {
 | 
				
			||||||
 | 
					            --border-color: #ff9900;
 | 
				
			||||||
 | 
					            .item {
 | 
				
			||||||
 | 
					                --color: var(--core-question-incorrect-color);
 | 
				
			||||||
 | 
					                --background: var(--core-question-incorrect-color-bg);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										200
									
								
								src/addons/qtype/ordering/component/ordering.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								src/addons/qtype/ordering/component/ordering.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,200 @@
 | 
				
			|||||||
 | 
					// (C) Copyright 2015 Moodle Pty Ltd.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					// you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					// You may obtain a copy of the License at
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					// distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					// See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					// limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Component, ElementRef } from '@angular/core';
 | 
				
			||||||
 | 
					import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
 | 
				
			||||||
 | 
					import { CoreQuestionHelper } from '@features/question/services/question-helper';
 | 
				
			||||||
 | 
					import { CoreDomUtils } from '@services/utils/dom';
 | 
				
			||||||
 | 
					import { ItemReorderEventDetail } from '@ionic/angular';
 | 
				
			||||||
 | 
					import { Translate } from '@singletons';
 | 
				
			||||||
 | 
					import { CoreUtils } from '@services/utils/utils';
 | 
				
			||||||
 | 
					import { CorePlatform } from '@services/platform';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Component to render an ordering question.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					    selector: 'addon-qtype-ordering',
 | 
				
			||||||
 | 
					    templateUrl: 'addon-qtype-ordering.html',
 | 
				
			||||||
 | 
					    styleUrls: ['../../../../core/features/question/question.scss', 'ordering.scss'],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AddonQtypeOrderingComponent extends CoreQuestionBaseComponent<AddonQtypeOrderingQuestionData> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dragDisabled = false;
 | 
				
			||||||
 | 
					    numberingClass = '';
 | 
				
			||||||
 | 
					    a11yAnnouncement = '';
 | 
				
			||||||
 | 
					    responseInput = {
 | 
				
			||||||
 | 
					        name: '',
 | 
				
			||||||
 | 
					        value: '',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(elementRef: ElementRef) {
 | 
				
			||||||
 | 
					        super('AddonQtypeOrderingComponent', elementRef);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @inheritdoc
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    init(): void {
 | 
				
			||||||
 | 
					        if (!this.question) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const questionElement = this.initComponent();
 | 
				
			||||||
 | 
					        if (!questionElement) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Replace Moodle's feedback classes with our own.
 | 
				
			||||||
 | 
					        CoreQuestionHelper.replaceFeedbackClasses(questionElement);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Find the list and its items.
 | 
				
			||||||
 | 
					        const listContainer = questionElement.querySelector('.sortablelist');
 | 
				
			||||||
 | 
					        if (!listContainer) {
 | 
				
			||||||
 | 
					            this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return CoreQuestionHelper.showComponentError(this.onAbort);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.dragDisabled = listContainer.classList.contains('notactive') || !listContainer.querySelector('.sortableitem');
 | 
				
			||||||
 | 
					        this.numberingClass = Array.from(listContainer.classList).find(className => className.startsWith('numbering')) ?? '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const itemsElements = Array.from(listContainer.querySelectorAll('li'));
 | 
				
			||||||
 | 
					        this.question.items = itemsElements.map(element => {
 | 
				
			||||||
 | 
					            // Remove correctness icons from the content.
 | 
				
			||||||
 | 
					            const itemContentEl = element.querySelector<HTMLElement>('[data-itemcontent]');
 | 
				
			||||||
 | 
					            itemContentEl?.querySelector('.icon.fa-check, .icon.fa-remove, .icon.fa-check-square')?.remove();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                id: element.id,
 | 
				
			||||||
 | 
					                content: itemContentEl?.innerHTML ?? '',
 | 
				
			||||||
 | 
					                contentText: itemContentEl?.innerText ?? '',
 | 
				
			||||||
 | 
					                correctClass: Array.from(element.classList)
 | 
				
			||||||
 | 
					                    .find(className => className.includes('correct') || className.includes('partial')) ?? 'pending',
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Find the input where the answer is stored.
 | 
				
			||||||
 | 
					        const inputEl = questionElement.querySelector<HTMLInputElement>('input[name*="_response_"]');
 | 
				
			||||||
 | 
					        if (inputEl) {
 | 
				
			||||||
 | 
					            this.responseInput.name = inputEl.name;
 | 
				
			||||||
 | 
					            this.responseInput.value = inputEl.value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Re-calculate the text of the question, removing the elements that the app already renders.
 | 
				
			||||||
 | 
					        questionElement.querySelector('.ablock')?.remove();
 | 
				
			||||||
 | 
					        inputEl?.remove();
 | 
				
			||||||
 | 
					        this.question.text = CoreDomUtils.getContentsOfElement(questionElement, '.qtext');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Reorder items list.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param eventDetail Details of the reorder.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    moveItem(eventDetail: ItemReorderEventDetail): void {
 | 
				
			||||||
 | 
					        if (!this.question?.items) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const itemToMove = this.question.items.splice(eventDetail.from, 1)[0];
 | 
				
			||||||
 | 
					        this.question.items.splice(eventDetail.to, 0, itemToMove);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.responseInput.value = this.question.items.map(item => item.id).join(',');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.a11yAnnouncement = Translate.instant('addon.qtype_ordering.moved', {
 | 
				
			||||||
 | 
					            $a: {
 | 
				
			||||||
 | 
					                item: this.hostElement.querySelector<HTMLElement>(`#${itemToMove.id}-text`)?.innerText ?? itemToMove.id,
 | 
				
			||||||
 | 
					                position: eventDetail.to + 1,
 | 
				
			||||||
 | 
					                total: this.question.items.length,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        eventDetail.complete();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Move an item to the previous or next position.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param event Event.
 | 
				
			||||||
 | 
					     * @param moveNext Whether to move to the next position or the previous position.
 | 
				
			||||||
 | 
					     * @param itemId Item ID.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async moveItemByClick(event: Event, moveNext: boolean, itemId: string): Promise<void> {
 | 
				
			||||||
 | 
					        event.preventDefault();
 | 
				
			||||||
 | 
					        event.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const target = event.target as HTMLElement;
 | 
				
			||||||
 | 
					        if (!target || !this.question?.items) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const initialPosition = this.question.items.findIndex(item => item.id === itemId);
 | 
				
			||||||
 | 
					        const endPosition = moveNext ? initialPosition + 1 : initialPosition - 1;
 | 
				
			||||||
 | 
					        if (endPosition < 0 || endPosition >= this.question.items.length) {
 | 
				
			||||||
 | 
					            // Invalid position.
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.moveItem({
 | 
				
			||||||
 | 
					            from: initialPosition,
 | 
				
			||||||
 | 
					            to: endPosition,
 | 
				
			||||||
 | 
					            complete: () => {}, // eslint-disable-line @typescript-eslint/no-empty-function
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await CoreUtils.nextTick();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // When moving an item to the first or last position, the button that was clicked will be hidden. In this case, we need to
 | 
				
			||||||
 | 
					        // focus the other button. Otherwise, re-focus the same button since the focus is lost in some cases.
 | 
				
			||||||
 | 
					        const movedCard = document.querySelector<HTMLElement>(`#${itemId}`);
 | 
				
			||||||
 | 
					        let elementToFocus = target;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (movedCard && !movedCard.previousElementSibling) {
 | 
				
			||||||
 | 
					            elementToFocus = movedCard.querySelector<HTMLElement>('[data-action="move-forward"]') ?? target;
 | 
				
			||||||
 | 
					        } else if (movedCard && !movedCard.nextElementSibling) {
 | 
				
			||||||
 | 
					            elementToFocus = movedCard.querySelector<HTMLElement>('[data-action="move-backward"]') ?? target;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CoreDomUtils.focusElement(elementToFocus);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (CorePlatform.isIOS()) {
 | 
				
			||||||
 | 
					            // In iOS, when the focus is lost VoiceOver automatically focus the element in the same position where the focus was.
 | 
				
			||||||
 | 
					            // If that happens, make sure the focus stays in the button we want to focus.
 | 
				
			||||||
 | 
					            const reFocus = () => {
 | 
				
			||||||
 | 
					                elementToFocus.removeEventListener('blur', reFocus);
 | 
				
			||||||
 | 
					                CoreDomUtils.focusElement(elementToFocus);
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            elementToFocus.addEventListener('blur', reFocus);
 | 
				
			||||||
 | 
					            setTimeout(() => {
 | 
				
			||||||
 | 
					                elementToFocus.removeEventListener('blur', reFocus);
 | 
				
			||||||
 | 
					            }, 300);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Data for ordering question.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export type AddonQtypeOrderingQuestionData = AddonModQuizQuestionBasicData & {
 | 
				
			||||||
 | 
					    readOnly?: boolean;
 | 
				
			||||||
 | 
					    items?: OrderingItem[];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type OrderingItem = {
 | 
				
			||||||
 | 
					    id: string;
 | 
				
			||||||
 | 
					    content: string;
 | 
				
			||||||
 | 
					    contentText: string;
 | 
				
			||||||
 | 
					    correctClass: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										3
									
								
								src/addons/qtype/ordering/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addons/qtype/ordering/lang.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "moved": "{{$a.item}} moved. New position: {{$a.position}} of {{$a.total}}."
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										41
									
								
								src/addons/qtype/ordering/ordering.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/addons/qtype/ordering/ordering.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					// (C) Copyright 2015 Moodle Pty Ltd.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					// you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					// You may obtain a copy of the License at
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					// distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					// See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					// limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { APP_INITIALIZER, NgModule } from '@angular/core';
 | 
				
			||||||
 | 
					import { CoreQuestionDelegate } from '@features/question/services/question-delegate';
 | 
				
			||||||
 | 
					import { AddonQtypeOrderingHandler } from './services/handlers/ordering';
 | 
				
			||||||
 | 
					import { AddonQtypeOrderingComponent } from './component/ordering';
 | 
				
			||||||
 | 
					import { CoreSharedModule } from '@/core/shared.module';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@NgModule({
 | 
				
			||||||
 | 
					    declarations: [
 | 
				
			||||||
 | 
					        AddonQtypeOrderingComponent,
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    imports: [
 | 
				
			||||||
 | 
					        CoreSharedModule,
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    providers: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            provide: APP_INITIALIZER,
 | 
				
			||||||
 | 
					            multi: true,
 | 
				
			||||||
 | 
					            useValue: () => {
 | 
				
			||||||
 | 
					                CoreQuestionDelegate.registerHandler(AddonQtypeOrderingHandler.instance);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    exports: [
 | 
				
			||||||
 | 
					        AddonQtypeOrderingComponent,
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AddonQtypeOrderingModule {}
 | 
				
			||||||
							
								
								
									
										73
									
								
								src/addons/qtype/ordering/services/handlers/ordering.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/addons/qtype/ordering/services/handlers/ordering.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					// (C) Copyright 2015 Moodle Pty Ltd.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					// you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					// You may obtain a copy of the License at
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					// distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					// See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					// limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Injectable, Type } from '@angular/core';
 | 
				
			||||||
 | 
					import { CoreQuestionHandler } from '@features/question/services/question-delegate';
 | 
				
			||||||
 | 
					import { CoreQuestion, CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question';
 | 
				
			||||||
 | 
					import { makeSingleton } from '@singletons';
 | 
				
			||||||
 | 
					import { CoreSites } from '@services/sites';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Handler to support ordering question type.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Injectable({ providedIn: 'root' })
 | 
				
			||||||
 | 
					export class AddonQtypeOrderingHandlerService implements CoreQuestionHandler {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name = 'AddonQtypeOrdering';
 | 
				
			||||||
 | 
					    type = 'qtype_ordering';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @inheritdoc
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async getComponent(): Promise<Type<unknown>> {
 | 
				
			||||||
 | 
					        const { AddonQtypeOrderingComponent } = await import('@addons/qtype/ordering/component/ordering');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return AddonQtypeOrderingComponent;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @inheritdoc
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    isCompleteResponse(): number {
 | 
				
			||||||
 | 
					        return 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @inheritdoc
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async isEnabled(): Promise<boolean> {
 | 
				
			||||||
 | 
					        return !!CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('4.4');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @inheritdoc
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    isGradableResponse(): number {
 | 
				
			||||||
 | 
					        return 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @inheritdoc
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    isSameResponse(
 | 
				
			||||||
 | 
					        question: CoreQuestionQuestionParsed,
 | 
				
			||||||
 | 
					        prevAnswers: CoreQuestionsAnswers,
 | 
				
			||||||
 | 
					        newAnswers: CoreQuestionsAnswers,
 | 
				
			||||||
 | 
					    ): boolean {
 | 
				
			||||||
 | 
					        return CoreQuestion.compareAllAnswers(prevAnswers, newAnswers);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AddonQtypeOrderingHandler = makeSingleton(AddonQtypeOrderingHandlerService);
 | 
				
			||||||
@ -27,6 +27,7 @@ import { AddonQtypeMatchModule } from './match/match.module';
 | 
				
			|||||||
import { AddonQtypeMultiAnswerModule } from './multianswer/multianswer.module';
 | 
					import { AddonQtypeMultiAnswerModule } from './multianswer/multianswer.module';
 | 
				
			||||||
import { AddonQtypeMultichoiceModule } from './multichoice/multichoice.module';
 | 
					import { AddonQtypeMultichoiceModule } from './multichoice/multichoice.module';
 | 
				
			||||||
import { AddonQtypeNumericalModule } from './numerical/numerical.module';
 | 
					import { AddonQtypeNumericalModule } from './numerical/numerical.module';
 | 
				
			||||||
 | 
					import { AddonQtypeOrderingModule } from './ordering/ordering.module';
 | 
				
			||||||
import { AddonQtypeRandomSaMatchModule } from './randomsamatch/randomsamatch.module';
 | 
					import { AddonQtypeRandomSaMatchModule } from './randomsamatch/randomsamatch.module';
 | 
				
			||||||
import { AddonQtypeShortAnswerModule } from './shortanswer/shortanswer.module';
 | 
					import { AddonQtypeShortAnswerModule } from './shortanswer/shortanswer.module';
 | 
				
			||||||
import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
 | 
					import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
 | 
				
			||||||
@ -46,6 +47,7 @@ import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
 | 
				
			|||||||
        AddonQtypeMultiAnswerModule,
 | 
					        AddonQtypeMultiAnswerModule,
 | 
				
			||||||
        AddonQtypeMultichoiceModule,
 | 
					        AddonQtypeMultichoiceModule,
 | 
				
			||||||
        AddonQtypeNumericalModule,
 | 
					        AddonQtypeNumericalModule,
 | 
				
			||||||
 | 
					        AddonQtypeOrderingModule,
 | 
				
			||||||
        AddonQtypeRandomSaMatchModule,
 | 
					        AddonQtypeRandomSaMatchModule,
 | 
				
			||||||
        AddonQtypeShortAnswerModule,
 | 
					        AddonQtypeShortAnswerModule,
 | 
				
			||||||
        AddonQtypeTrueFalseModule,
 | 
					        AddonQtypeTrueFalseModule,
 | 
				
			||||||
 | 
				
			|||||||
@ -19,6 +19,10 @@
 | 
				
			|||||||
            margin: 0 0 .5em;
 | 
					            margin: 0 0 .5em;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        p {
 | 
				
			||||||
 | 
					            --color: var(--core-question-feedback-color);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        .correctness {
 | 
					        .correctness {
 | 
				
			||||||
            display: inline-block;
 | 
					            display: inline-block;
 | 
				
			||||||
            padding: 2px 4px;
 | 
					            padding: 2px 4px;
 | 
				
			||||||
 | 
				
			|||||||
@ -204,6 +204,8 @@
 | 
				
			|||||||
    "mod_workshop": "Workshop",
 | 
					    "mod_workshop": "Workshop",
 | 
				
			||||||
    "moduleintro": "Description",
 | 
					    "moduleintro": "Description",
 | 
				
			||||||
    "more": "More",
 | 
					    "more": "More",
 | 
				
			||||||
 | 
					    "movedown": "Move down",
 | 
				
			||||||
 | 
					    "moveup": "Move up",
 | 
				
			||||||
    "mygroups": "My groups",
 | 
					    "mygroups": "My groups",
 | 
				
			||||||
    "name": "Name",
 | 
					    "name": "Name",
 | 
				
			||||||
    "needhelp": "Need help?",
 | 
					    "needhelp": "Need help?",
 | 
				
			||||||
 | 
				
			|||||||
@ -295,10 +295,11 @@ export class CoreDomUtilsProvider {
 | 
				
			|||||||
     * @param element HTML element to focus.
 | 
					     * @param element HTML element to focus.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    async focusElement(
 | 
					    async focusElement(
 | 
				
			||||||
        element: HTMLIonInputElement | HTMLIonTextareaElement | HTMLIonSearchbarElement | HTMLElement,
 | 
					        element: HTMLIonInputElement | HTMLIonTextareaElement | HTMLIonSearchbarElement | HTMLIonButtonElement | HTMLElement,
 | 
				
			||||||
    ): Promise<void> {
 | 
					    ): Promise<void> {
 | 
				
			||||||
        let retries = 10;
 | 
					        let retries = 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const isIonButton = element.tagName === 'ION-BUTTON';
 | 
				
			||||||
        let elementToFocus = element;
 | 
					        let elementToFocus = element;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
@ -319,6 +320,10 @@ export class CoreDomUtilsProvider {
 | 
				
			|||||||
            // If it's an Ionic element get the right input to use.
 | 
					            // If it's an Ionic element get the right input to use.
 | 
				
			||||||
            elementToFocus.componentOnReady && await elementToFocus.componentOnReady();
 | 
					            elementToFocus.componentOnReady && await elementToFocus.componentOnReady();
 | 
				
			||||||
            elementToFocus = await elementToFocus.getInputElement();
 | 
					            elementToFocus = await elementToFocus.getInputElement();
 | 
				
			||||||
 | 
					        } else if (isIonButton) {
 | 
				
			||||||
 | 
					            // For ion-button, we need to call focus on the inner button. But the activeElement will be the ion-button.
 | 
				
			||||||
 | 
					            ('componentOnReady' in elementToFocus) && await elementToFocus.componentOnReady();
 | 
				
			||||||
 | 
					            elementToFocus = elementToFocus.shadowRoot?.querySelector('.button-native') ?? elementToFocus;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!elementToFocus || !elementToFocus.focus) {
 | 
					        if (!elementToFocus || !elementToFocus.focus) {
 | 
				
			||||||
@ -328,7 +333,7 @@ export class CoreDomUtilsProvider {
 | 
				
			|||||||
        while (retries > 0 && elementToFocus !== document.activeElement) {
 | 
					        while (retries > 0 && elementToFocus !== document.activeElement) {
 | 
				
			||||||
            elementToFocus.focus();
 | 
					            elementToFocus.focus();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (elementToFocus === document.activeElement) {
 | 
					            if (elementToFocus === document.activeElement || (isIonButton && element === document.activeElement)) {
 | 
				
			||||||
                await CoreUtils.nextTick();
 | 
					                await CoreUtils.nextTick();
 | 
				
			||||||
                if (CorePlatform.isAndroid() && this.supportsInputKeyboard(elementToFocus)) {
 | 
					                if (CorePlatform.isAndroid() && this.supportsInputKeyboard(elementToFocus)) {
 | 
				
			||||||
                    // On some Android versions the keyboard doesn't open automatically.
 | 
					                    // On some Android versions the keyboard doesn't open automatically.
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user