Merge pull request #4176 from dpalou/MOBILE-4587

MOBILE-4587 qtype: Fix race condition with MathJax in D&D questions
main
Pau Ferrer Ocaña 2024-09-17 11:10:16 +02:00 committed by GitHub
commit 437f309957
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 45 additions and 28 deletions

View File

@ -177,7 +177,7 @@ export class AddonFilterMathJaxLoaderHandlerService extends CoreFilterDefaultHan
): Promise<void> {
await this.waitForReady();
this.window.M!.filter_mathjaxloader!.typeset(container);
await this.window.M!.filter_mathjaxloader!.typeset(container);
}
/**
@ -234,24 +234,32 @@ export class AddonFilterMathJaxLoaderHandlerService extends CoreFilterDefaultHan
}
},
// Called by the filter when an equation is found while rendering the page.
typeset: function (container: HTMLElement): void {
typeset: async function (container: HTMLElement): Promise<void> {
if (!this._configured) {
this._setLocale();
}
if (that.window.MathJax !== undefined) {
const processDelay = that.window.MathJax.Hub.processSectionDelay;
// Set the process section delay to 0 when updating the formula.
that.window.MathJax.Hub.processSectionDelay = 0;
const equations = Array.from(container.querySelectorAll('.filter_mathjaxloader_equation'));
equations.forEach((node) => {
that.window.MathJax.Hub.Queue(['Typeset', that.window.MathJax.Hub, node], [that.fixUseUrls, node]);
});
// Set the delay back to normal after processing.
that.window.MathJax.Hub.processSectionDelay = processDelay;
if (that.window.MathJax === undefined) {
return;
}
const processDelay = that.window.MathJax.Hub.processSectionDelay;
// Set the process section delay to 0 when updating the formula.
that.window.MathJax.Hub.processSectionDelay = 0;
const equations = Array.from(container.querySelectorAll('.filter_mathjaxloader_equation'));
const promises = equations.map((node) => new Promise<void>((resolve) => {
that.window.MathJax.Hub.Queue(
['Typeset', that.window.MathJax.Hub, node],
[that.fixUseUrls, node],
[resolve],
);
}));
// Set the delay back to normal after processing.
that.window.MathJax.Hub.processSectionDelay = processDelay;
await Promise.all(promises);
},
};
}

View File

@ -18,6 +18,6 @@
<div class="fake-ion-item ion-text-wrap" [class.readonly]="question.readOnly" [hidden]="!question.loaded">
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
[text]="question.ddArea" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId"
(afterRender)="ddAreaRendered()" />
(filterContentRenderingComplete)="ddAreaRendered()" />
</div>
</div>

View File

@ -18,6 +18,6 @@
<div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
[text]="question.ddArea" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId"
(afterRender)="ddAreaRendered()" />
(filterContentRenderingComplete)="ddAreaRendered()" />
</div>
</div>

View File

@ -13,7 +13,6 @@
// limitations under the License.
import { CoreFormatTextDirective } from '@directives/format-text';
import { CoreText } from '@singletons/text';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreCoordinates, CoreDom } from '@singletons/dom';
import { CoreEventObserver } from '@singletons/events';
@ -489,10 +488,6 @@ export class AddonQtypeDdwtosQuestion {
return;
}
groupItems.forEach((item) => {
item.innerHTML = CoreText.decodeHTML(item.innerHTML);
});
// Wait to render in order to calculate size.
if (groupItems[0].parentElement) {
// Wait for parent to be visible. We cannot wait for group items because they have visibility hidden.

View File

@ -16,7 +16,7 @@
<core-format-text *ngIf="question.answers" [component]="component" [componentId]="componentId" [text]="question.answers"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId"
(afterRender)="answersRendered()" />
(filterContentRenderingComplete)="answersRendered()" />
</div>
</div>
</div>

View File

@ -18,6 +18,7 @@ import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@featu
import { CoreQuestionHelper } from '@features/question/services/question-helper';
import { CoreDomUtils } from '@services/utils/dom';
import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos';
import { CoreText } from '@singletons/text';
/**
* Component to render a drag-and-drop words into sentences question.
@ -69,6 +70,13 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent<AddonMo
}
this.question.readOnly = answerContainer.classList.contains('readonly');
// Decode content of drag homes. This must be done before filters are applied, otherwise some things don't work as expected.
const groupItems = Array.from(answerContainer.querySelectorAll<HTMLElement>('span.draghome'));
groupItems.forEach((item) => {
item.innerHTML = CoreText.decodeHTML(item.innerHTML);
});
// Add the drags container inside the answers so it's rendered inside core-format-text,
// otherwise some styles could be different between the drag homes and the draggables.
this.question.answers = answerContainer.outerHTML + '<div class="drags"></div>';

View File

@ -94,7 +94,8 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
@Input({ transform: toBoolean }) hideIfEmpty = false; // If true, the tag will contain nothing if text is empty.
@Input({ transform: toBoolean }) disabled = false; // If disabled, autoplay elements will be disabled.
@Output() afterRender: EventEmitter<void>; // Called when the data is rendered.
@Output() afterRender = new EventEmitter<void>(); // Called when the data is rendered.
@Output() filterContentRenderingComplete = new EventEmitter<void>(); // Called when the filters have finished rendering content.
@Output() onClick: EventEmitter<void> = new EventEmitter(); // Called when clicked.
protected element: HTMLElement;
@ -117,8 +118,6 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
this.emptyText = this.hideIfEmpty ? '' : '&nbsp;';
this.element.innerHTML = this.emptyText;
this.afterRender = new EventEmitter<void>();
this.element.addEventListener('click', (event) => this.elementClicked(event));
this.siteId = this.siteId || CoreSites.getCurrentSiteId();
@ -340,8 +339,10 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
/**
* Finish the rendering, displaying the element again and calling afterRender.
*
* @param triggerFilterRender Whether to emit the filterContentRenderingComplete output too.
*/
protected async finishRender(): Promise<void> {
protected async finishRender(triggerFilterRender = true): Promise<void> {
// Show the element again.
this.element.classList.remove('core-loading');
@ -349,6 +350,9 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
// Emit the afterRender output.
this.afterRender.emit();
if (triggerFilterRender) {
this.filterContentRenderingComplete.emit();
}
}
/**
@ -402,11 +406,13 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
this.component,
this.componentId,
result.siteId,
);
).finally(() => {
this.filterContentRenderingComplete.emit();
});
}
this.element.classList.remove('core-disable-media-adapt');
await this.finishRender();
await this.finishRender(!result.options.filter);
}
/**