MOBILE-4457 question: Support ordering question type
parent
86477e69c5
commit
5cee8d4935
|
@ -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>
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"moved": "{{$a.item}} moved. New position: {{$a.position}} of {{$a.total}}."
|
||||||
|
}
|
|
@ -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 {}
|
|
@ -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…
Reference in New Issue