commit
be28e1c2e4
|
@ -21,6 +21,7 @@ import { ContextLevel, CoreConstants } from '@/core/constants';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to render an "activity modules" block.
|
* Component to render an "activity modules" block.
|
||||||
|
@ -67,7 +68,7 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i
|
||||||
}
|
}
|
||||||
|
|
||||||
section.modules.forEach((mod) => {
|
section.modules.forEach((mod) => {
|
||||||
if (mod.uservisible === false || !CoreCourse.moduleHasView(mod) ||
|
if (!CoreCourseHelper.canUserViewModule(mod, section) || !CoreCourse.moduleHasView(mod) ||
|
||||||
modFullNames[mod.modname] !== undefined) {
|
modFullNames[mod.modname] !== undefined) {
|
||||||
// Ignore this module.
|
// Ignore this module.
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -183,7 +183,7 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item class="ion-text-wrap">
|
<ion-item class="ion-text-wrap">
|
||||||
<ion-label position="stacked">
|
<ion-label position="stacked">
|
||||||
<p>{{ 'addon.calendar.repeatweeksl' | translate }}</p>
|
<p class="item-heading">{{ 'addon.calendar.repeatweeksl' | translate }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-input type="number" name="repeats" formControlName="repeats" [disabled]="!form.controls.repeat.value">
|
<ion-input type="number" name="repeats" formControlName="repeats" [disabled]="!form.controls.repeat.value">
|
||||||
</ion-input>
|
</ion-input>
|
||||||
|
@ -231,23 +231,17 @@
|
||||||
<ion-input type="text" name="location" [placeholder]="'core.location' | translate" formControlName="location">
|
<ion-input type="text" name="location" [placeholder]="'core.location' | translate" formControlName="location">
|
||||||
</ion-input>
|
</ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
</form>
|
||||||
<ion-item>
|
<div collapsible-footer appearOnBottom *ngIf="loaded && !error" slot="fixed">
|
||||||
<ion-label>
|
<div class="list-item-limited-width adaptable-buttons-row">
|
||||||
<ion-row>
|
<ion-button *ngIf="hasOffline && eventId && eventId < 0" expand="block" fill="outline" (click)="discard()"
|
||||||
<ion-col *ngIf="hasOffline && eventId && eventId < 0">
|
class="ion-margin ion-text-wrap">
|
||||||
<ion-button expand="block" fill="outline" (click)="discard()">
|
|
||||||
{{ 'core.discard' | translate }}
|
{{ 'core.discard' | translate }}
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-col>
|
<ion-button expand="block" (click)="submit()" [disabled]="!form.valid" type="submit" class="ion-margin ion-text-wrap">
|
||||||
<ion-col>
|
|
||||||
<ion-button expand="block" (click)="submit()" [disabled]="!form.valid" type="submit">
|
|
||||||
{{ 'core.save' | translate }}
|
{{ 'core.save' | translate }}
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-col>
|
</div>
|
||||||
</ion-row>
|
</div>
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</form>
|
|
||||||
</core-loading>
|
</core-loading>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter';
|
import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter';
|
||||||
import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter';
|
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreUrlUtils } from '@services/utils/url';
|
import { CoreUrlUtils } from '@services/utils/url';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
|
@ -32,19 +31,10 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
|
||||||
protected template = document.createElement('template'); // A template element to convert HTML to element.
|
protected template = document.createElement('template'); // A template element to convert HTML to element.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter some text.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param text The text to filter.
|
|
||||||
* @param filter The filter.
|
|
||||||
* @param options Options passed to the filters.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
|
||||||
* @return Filtered text (or promise resolved with the filtered text).
|
|
||||||
*/
|
*/
|
||||||
filter(
|
filter(
|
||||||
text: string,
|
text: string,
|
||||||
filter: CoreFilterFilter, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
||||||
options: CoreFilterFormatTextOptions, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
||||||
siteId?: string, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
||||||
): string | Promise<string> {
|
): string | Promise<string> {
|
||||||
this.template.innerHTML = text;
|
this.template.innerHTML = text;
|
||||||
|
|
||||||
|
@ -60,7 +50,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
|
||||||
/**
|
/**
|
||||||
* Treat video filters. Currently only treating youtube video using video JS.
|
* Treat video filters. Currently only treating youtube video using video JS.
|
||||||
*
|
*
|
||||||
* @param el Video element.
|
* @param video Video element.
|
||||||
*/
|
*/
|
||||||
protected treatVideoFilters(video: HTMLElement): void {
|
protected treatVideoFilters(video: HTMLElement): void {
|
||||||
// Treat Video JS Youtube video links and translate them to iframes.
|
// Treat Video JS Youtube video links and translate them to iframes.
|
||||||
|
|
|
@ -1106,10 +1106,10 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
||||||
* Scroll to the first new unread message.
|
* Scroll to the first new unread message.
|
||||||
*/
|
*/
|
||||||
scrollToFirstUnreadMessage(): void {
|
scrollToFirstUnreadMessage(): void {
|
||||||
if (this.newMessages > 0 && this.content) {
|
if (this.newMessages > 0) {
|
||||||
const messages = Array.from(this.hostElement.querySelectorAll('.addon-message-not-mine'));
|
const messages = Array.from(this.hostElement.querySelectorAll<HTMLElement>('.addon-message-not-mine'));
|
||||||
|
|
||||||
CoreDomUtils.scrollToElement(this.content, <HTMLElement> messages[messages.length - this.newMessages]);
|
CoreDomUtils.scrollViewToElement(messages[messages.length - this.newMessages]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -159,7 +159,7 @@ export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave {
|
||||||
this.messages[this.messages.length - 1].showTail = true;
|
this.messages[this.messages.length - 1].showTail = true;
|
||||||
|
|
||||||
// New messages or beeps, scroll to bottom.
|
// New messages or beeps, scroll to bottom.
|
||||||
setTimeout(() => this.scrollToBottom());
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadMessageBeepWho(message: AddonModChatFormattedMessage): Promise<void> {
|
protected async loadMessageBeepWho(message: AddonModChatFormattedMessage): Promise<void> {
|
||||||
|
@ -341,13 +341,12 @@ export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave {
|
||||||
/**
|
/**
|
||||||
* Scroll bottom when render has finished.
|
* Scroll bottom when render has finished.
|
||||||
*/
|
*/
|
||||||
scrollToBottom(): void {
|
async scrollToBottom(): Promise<void> {
|
||||||
// Need a timeout to leave time to the view to be rendered.
|
// Need a timeout to leave time to the view to be rendered.
|
||||||
setTimeout(() => {
|
await CoreUtils.nextTick();
|
||||||
if (!this.viewDestroyed) {
|
if (!this.viewDestroyed) {
|
||||||
this.content?.scrollToBottom();
|
this.content?.scrollToBottom();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -352,9 +352,7 @@ export class AddonModDataEditPage implements OnInit {
|
||||||
}
|
}
|
||||||
this.jsData!.errors = this.errors;
|
this.jsData!.errors = this.errors;
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.scrollToFirstError();
|
this.scrollToFirstError();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
modal.dismiss();
|
modal.dismiss();
|
||||||
|
@ -449,8 +447,9 @@ export class AddonModDataEditPage implements OnInit {
|
||||||
/**
|
/**
|
||||||
* Scroll to first error or to the top if not found.
|
* Scroll to first error or to the top if not found.
|
||||||
*/
|
*/
|
||||||
protected scrollToFirstError(): void {
|
protected async scrollToFirstError(): Promise<void> {
|
||||||
if (!CoreDomUtils.scrollToElementBySelector(this.formElement.nativeElement, this.content, '.addon-data-error')) {
|
const scrolled = await CoreDomUtils.scrollViewToElement(this.formElement.nativeElement, '.addon-data-error');
|
||||||
|
if (!scrolled) {
|
||||||
this.content?.scrollToTop();
|
this.content?.scrollToTop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import {
|
||||||
OnChanges,
|
OnChanges,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
Optional,
|
|
||||||
Output,
|
Output,
|
||||||
SimpleChange,
|
SimpleChange,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
|
@ -41,7 +40,6 @@ import {
|
||||||
import { CoreTag } from '@features/tag/services/tag';
|
import { CoreTag } from '@features/tag/services/tag';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||||
import { IonContent } from '@ionic/angular';
|
|
||||||
import { AddonModForumSync } from '../../services/forum-sync';
|
import { AddonModForumSync } from '../../services/forum-sync';
|
||||||
import { CoreSync } from '@services/sync';
|
import { CoreSync } from '@services/sync';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
@ -94,7 +92,6 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected elementRef: ElementRef,
|
protected elementRef: ElementRef,
|
||||||
@Optional() protected content?: IonContent,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get showForm(): boolean {
|
get showForm(): boolean {
|
||||||
|
@ -308,8 +305,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
||||||
this.post.id > 0 ? this.post.id : undefined,
|
this.post.id > 0 ? this.post.id : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.scrollToForm(5);
|
this.scrollToForm();
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Cancelled.
|
// Cancelled.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -540,19 +537,11 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
||||||
/**
|
/**
|
||||||
* Scroll to reply/edit form.
|
* Scroll to reply/edit form.
|
||||||
*
|
*
|
||||||
* @param ticksToWait Number of ticks to wait before scrolling.
|
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async scrollToForm(ticksToWait = 1): Promise<void> {
|
protected async scrollToForm(): Promise<void> {
|
||||||
if (!this.content) {
|
await CoreDomUtils.scrollViewToElement(
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await CoreUtils.nextTicks(ticksToWait);
|
|
||||||
|
|
||||||
CoreDomUtils.scrollToElementBySelector(
|
|
||||||
this.elementRef.nativeElement,
|
this.elementRef.nativeElement,
|
||||||
this.content,
|
|
||||||
'#addon-forum-reply-edit-form-' + this.uniqueId,
|
'#addon-forum-reply-edit-form-' + this.uniqueId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -187,13 +187,10 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
||||||
const scrollTo = this.postId || this.parent;
|
const scrollTo = this.postId || this.parent;
|
||||||
if (scrollTo) {
|
if (scrollTo) {
|
||||||
// Scroll to the post.
|
// Scroll to the post.
|
||||||
setTimeout(() => {
|
CoreDomUtils.scrollViewToElement(
|
||||||
CoreDomUtils.scrollToElementBySelector(
|
|
||||||
this.elementRef.nativeElement,
|
this.elementRef.nativeElement,
|
||||||
this.content,
|
|
||||||
'#addon-mod_forum-post-' + scrollTo,
|
'#addon-mod_forum-post-' + scrollTo,
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
|
|
||||||
import { Component, OnInit, ViewChild, ElementRef, Input, Type } from '@angular/core';
|
import { Component, OnInit, ViewChild, ElementRef, Input, Type } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
import { IonContent } from '@ionic/angular';
|
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
|
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
@ -32,7 +31,6 @@ import { AddonModQuizAttemptWSData, AddonModQuizQuizWSData } from '../../service
|
||||||
})
|
})
|
||||||
export class AddonModQuizPreflightModalComponent implements OnInit {
|
export class AddonModQuizPreflightModalComponent implements OnInit {
|
||||||
|
|
||||||
@ViewChild(IonContent) content?: IonContent;
|
|
||||||
@ViewChild('preflightFormEl') formElement?: ElementRef;
|
@ViewChild('preflightFormEl') formElement?: ElementRef;
|
||||||
|
|
||||||
@Input() title!: string;
|
@Input() title!: string;
|
||||||
|
@ -111,15 +109,14 @@ export class AddonModQuizPreflightModalComponent implements OnInit {
|
||||||
*
|
*
|
||||||
* @param e Event.
|
* @param e Event.
|
||||||
*/
|
*/
|
||||||
sendData(e: Event): void {
|
async sendData(e: Event): Promise<void> {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (!this.preflightForm.valid) {
|
if (!this.preflightForm.valid) {
|
||||||
// Form not valid. Scroll to the first element with errors.
|
// Form not valid. Scroll to the first element with errors.
|
||||||
const hasScrolled = CoreDomUtils.scrollToInputError(
|
const hasScrolled = await CoreDomUtils.scrollViewToInputError(
|
||||||
this.elementRef.nativeElement,
|
this.elementRef.nativeElement,
|
||||||
this.content,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasScrolled) {
|
if (!hasScrolled) {
|
||||||
|
|
|
@ -318,10 +318,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
|
|
||||||
if (slot !== undefined) {
|
if (slot !== undefined) {
|
||||||
// Scroll to the question. Give some time to the questions to render.
|
// Scroll to the question.
|
||||||
setTimeout(() => {
|
|
||||||
this.scrollToQuestion(slot);
|
this.scrollToQuestion(slot);
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -689,9 +687,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
* @param slot Slot of the question to scroll to.
|
* @param slot Slot of the question to scroll to.
|
||||||
*/
|
*/
|
||||||
protected scrollToQuestion(slot: number): void {
|
protected scrollToQuestion(slot: number): void {
|
||||||
CoreDomUtils.scrollToElementBySelector(
|
CoreDomUtils.scrollViewToElement(
|
||||||
this.elementRef.nativeElement,
|
this.elementRef.nativeElement,
|
||||||
this.content,
|
|
||||||
'#addon-mod_quiz-question-' + slot,
|
'#addon-mod_quiz-question-' + slot,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,10 +133,8 @@ export class AddonModQuizReviewPage implements OnInit {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
|
|
||||||
if (slot !== undefined) {
|
if (slot !== undefined) {
|
||||||
// Scroll to the question. Give some time to the questions to render.
|
// Scroll to the question.
|
||||||
setTimeout(() => {
|
|
||||||
this.scrollToQuestion(slot);
|
this.scrollToQuestion(slot);
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -249,9 +247,8 @@ export class AddonModQuizReviewPage implements OnInit {
|
||||||
* @param slot Slot of the question to scroll to.
|
* @param slot Slot of the question to scroll to.
|
||||||
*/
|
*/
|
||||||
protected scrollToQuestion(slot: number): void {
|
protected scrollToQuestion(slot: number): void {
|
||||||
CoreDomUtils.scrollToElementBySelector(
|
CoreDomUtils.scrollViewToElement(
|
||||||
this.elementRef.nativeElement,
|
this.elementRef.nativeElement,
|
||||||
this.content,
|
|
||||||
`#addon-mod_quiz-question-${slot}`,
|
`#addon-mod_quiz-question-${slot}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,13 +78,18 @@ export class AddonQtypeDdImageOrTextQuestion {
|
||||||
return bgImgXY;
|
return bgImgXY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = CoreDomUtils.getElementXY(bgImg, undefined, 'ddarea');
|
const ddArea = this.container.querySelector<HTMLElement>('.ddarea');
|
||||||
|
if (!ddArea) {
|
||||||
|
return bgImgXY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = CoreDomUtils.getRelativeElementPosition(bgImg, ddArea);
|
||||||
|
|
||||||
// Render the position related to the current image dimensions.
|
// Render the position related to the current image dimensions.
|
||||||
bgImgXY[0] *= this.proportion;
|
bgImgXY[0] *= this.proportion;
|
||||||
bgImgXY[1] *= this.proportion;
|
bgImgXY[1] *= this.proportion;
|
||||||
|
|
||||||
return [Number(bgImgXY[0]) + position[0] + 1, Number(bgImgXY[1]) + position[1] + 1];
|
return [bgImgXY[0] + position.x + 1, bgImgXY[1] + position.y + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -360,7 +365,7 @@ export class AddonQtypeDdImageOrTextQuestion {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.resizeListener = CoreDomUtils.onWindowResize(() => {
|
this.resizeListener = CoreDomUtils.onWindowResize(() => {
|
||||||
this.windowResized();
|
this.repositionDragsForQuestion();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,10 +414,15 @@ export class AddonQtypeDdImageOrTextQuestion {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now position the draggable and set it to the input.
|
// Now position the draggable and set it to the input.
|
||||||
const position = CoreDomUtils.getElementXY(drop, undefined, 'ddarea');
|
const ddArea = this.container.querySelector<HTMLElement>('.ddarea');
|
||||||
|
if (!ddArea) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = CoreDomUtils.getRelativeElementPosition(drop, ddArea);
|
||||||
const choice = drag.getAttribute('choice');
|
const choice = drag.getAttribute('choice');
|
||||||
drag.style.left = position[0] - 1 + 'px';
|
drag.style.left = position.x + 'px';
|
||||||
drag.style.top = position[1] - 1 + 'px';
|
drag.style.top = position.y + 'px';
|
||||||
drag.classList.add('placed');
|
drag.classList.add('placed');
|
||||||
|
|
||||||
if (choice) {
|
if (choice) {
|
||||||
|
@ -458,13 +468,14 @@ export class AddonQtypeDdImageOrTextQuestion {
|
||||||
|
|
||||||
// Move the element to its original position.
|
// Move the element to its original position.
|
||||||
const dragItemHome = this.doc.dragItemHome(Number(drag.getAttribute('dragitemno')));
|
const dragItemHome = this.doc.dragItemHome(Number(drag.getAttribute('dragitemno')));
|
||||||
if (!dragItemHome) {
|
const ddArea = this.container.querySelector<HTMLElement>('.ddarea');
|
||||||
|
if (!dragItemHome || !ddArea) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = CoreDomUtils.getElementXY(dragItemHome, undefined, 'ddarea');
|
const position = CoreDomUtils.getRelativeElementPosition(dragItemHome, ddArea);
|
||||||
drag.style.left = position[0] + 'px';
|
drag.style.left = position.x + 'px';
|
||||||
drag.style.top = position[1] + 'px';
|
drag.style.top = position.y + 'px';
|
||||||
drag.classList.remove('placed');
|
drag.classList.remove('placed');
|
||||||
|
|
||||||
drag.setAttribute('inputid', '');
|
drag.setAttribute('inputid', '');
|
||||||
|
@ -679,15 +690,6 @@ export class AddonQtypeDdImageOrTextQuestion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Window resized.
|
|
||||||
*/
|
|
||||||
async windowResized(): Promise<void> {
|
|
||||||
await CoreDomUtils.waitForResizeDone();
|
|
||||||
|
|
||||||
this.repositionDragsForQuestion();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -12,21 +12,13 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreCoordinates, CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreEventObserver } from '@singletons/events';
|
import { CoreEventObserver } from '@singletons/events';
|
||||||
import { CoreLogger } from '@singletons/logger';
|
import { CoreLogger } from '@singletons/logger';
|
||||||
import { AddonQtypeDdMarkerQuestionData } from '../component/ddmarker';
|
import { AddonQtypeDdMarkerQuestionData } from '../component/ddmarker';
|
||||||
import { AddonQtypeDdMarkerGraphicsApi } from './graphics_api';
|
import { AddonQtypeDdMarkerGraphicsApi } from './graphics_api';
|
||||||
|
|
||||||
/**
|
|
||||||
* Point type.
|
|
||||||
*/
|
|
||||||
export type AddonQtypeDdMarkerQuestionPoint = {
|
|
||||||
x: number; // X axis coordinates.
|
|
||||||
y: number; // Y axis coordinates.
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to make a question of ddmarker type work.
|
* Class to make a question of ddmarker type work.
|
||||||
*/
|
*/
|
||||||
|
@ -36,8 +28,7 @@ export class AddonQtypeDdMarkerQuestion {
|
||||||
|
|
||||||
protected logger: CoreLogger;
|
protected logger: CoreLogger;
|
||||||
protected afterImageLoadDone = false;
|
protected afterImageLoadDone = false;
|
||||||
protected drops;
|
protected topNode?: HTMLElement | null;
|
||||||
protected topNode;
|
|
||||||
protected nextColourIndex = 0;
|
protected nextColourIndex = 0;
|
||||||
protected proportion = 1;
|
protected proportion = 1;
|
||||||
protected selected?: HTMLElement; // Selected element (being "dragged").
|
protected selected?: HTMLElement; // Selected element (being "dragged").
|
||||||
|
@ -123,7 +114,7 @@ export class AddonQtypeDdMarkerQuestion {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = CoreDomUtils.getElementXY(bgImg, undefined, 'ddarea');
|
const position = this.getElementCoordinates(bgImg);
|
||||||
let coordsNumbers = this.parsePoint(bgImgXY);
|
let coordsNumbers = this.parsePoint(bgImgXY);
|
||||||
|
|
||||||
coordsNumbers = this.makePointProportional(coordsNumbers);
|
coordsNumbers = this.makePointProportional(coordsNumbers);
|
||||||
|
@ -131,13 +122,30 @@ export class AddonQtypeDdMarkerQuestion {
|
||||||
return [coordsNumbers.x + position[0], coordsNumbers.y + position[1]];
|
return [coordsNumbers.x + position[0], coordsNumbers.y + position[1]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns elements coordinates relative to ddarea container.
|
||||||
|
*
|
||||||
|
* @param element Element.
|
||||||
|
* @return Array of X and Y coordinates.
|
||||||
|
*/
|
||||||
|
protected getElementCoordinates(element: HTMLElement): number[] {
|
||||||
|
const ddArea = this.container.querySelector<HTMLElement>('.ddarea');
|
||||||
|
if (!ddArea) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = CoreDomUtils.getRelativeElementPosition(element, ddArea);
|
||||||
|
|
||||||
|
return [position.x, position.y];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if some coordinates (X, Y) are inside the background image.
|
* Check if some coordinates (X, Y) are inside the background image.
|
||||||
*
|
*
|
||||||
* @param coords Coordinates to check.
|
* @param coords Coordinates to check.
|
||||||
* @return Whether they're inside the background image.
|
* @return Whether they're inside the background image.
|
||||||
*/
|
*/
|
||||||
coordsInImg(coords: AddonQtypeDdMarkerQuestionPoint): boolean {
|
coordsInImg(coords: CoreCoordinates): boolean {
|
||||||
const bgImg = this.doc.bgImg();
|
const bgImg = this.doc.bgImg();
|
||||||
if (!bgImg) {
|
if (!bgImg) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -177,13 +185,13 @@ export class AddonQtypeDdMarkerQuestion {
|
||||||
const dragging = this.selected;
|
const dragging = this.selected;
|
||||||
if (dragging && !drag.classList.contains('unplaced')) {
|
if (dragging && !drag.classList.contains('unplaced')) {
|
||||||
|
|
||||||
const position = CoreDomUtils.getElementXY(drag, undefined, 'ddarea');
|
const position = this.getElementCoordinates(drag);
|
||||||
const bgImg = this.doc.bgImg();
|
const bgImg = this.doc.bgImg();
|
||||||
if (!bgImg) {
|
if (!bgImg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bgImgPos = CoreDomUtils.getElementXY(bgImg, undefined, 'ddarea');
|
const bgImgPos = this.getElementCoordinates(bgImg);
|
||||||
|
|
||||||
position[0] = position[0] - bgImgPos[0] + e.offsetX;
|
position[0] = position[0] - bgImgPos[0] + e.offsetX;
|
||||||
position[1] = position[1] - bgImgPos[1] + e.offsetY;
|
position[1] = position[1] - bgImgPos[1] + e.offsetY;
|
||||||
|
@ -217,7 +225,7 @@ export class AddonQtypeDdMarkerQuestion {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = CoreDomUtils.getElementXY(dragItemHome, undefined, 'ddarea');
|
const position = this.getElementCoordinates(dragItemHome);
|
||||||
|
|
||||||
return [position[0], position[1]];
|
return [position[0], position[1]];
|
||||||
}
|
}
|
||||||
|
@ -317,7 +325,7 @@ export class AddonQtypeDdMarkerQuestion {
|
||||||
* @param colour Colour of the circle.
|
* @param colour Colour of the circle.
|
||||||
* @return X and Y position of the center of the circle.
|
* @return X and Y position of the center of the circle.
|
||||||
*/
|
*/
|
||||||
drawShapeCircle(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint | null {
|
drawShapeCircle(dropZoneNo: number, coordinates: string, colour: string): CoreCoordinates | null {
|
||||||
if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?$/)) {
|
if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?$/)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -356,7 +364,7 @@ export class AddonQtypeDdMarkerQuestion {
|
||||||
* @param colour Colour of the rectangle.
|
* @param colour Colour of the rectangle.
|
||||||
* @return X and Y position of the center of the rectangle.
|
* @return X and Y position of the center of the rectangle.
|
||||||
*/
|
*/
|
||||||
drawShapeRectangle(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint | null {
|
drawShapeRectangle(dropZoneNo: number, coordinates: string, colour: string): CoreCoordinates | null {
|
||||||
if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?,\d+(\.\d+)?$/)) {
|
if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?,\d+(\.\d+)?$/)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -399,7 +407,7 @@ export class AddonQtypeDdMarkerQuestion {
|
||||||
* @param colour Colour of the polygon.
|
* @param colour Colour of the polygon.
|
||||||
* @return X and Y position of the center of the polygon.
|
* @return X and Y position of the center of the polygon.
|
||||||
*/
|
*/
|
||||||
drawShapePolygon(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint | null {
|
drawShapePolygon(dropZoneNo: number, coordinates: string, colour: string): CoreCoordinates | null {
|
||||||
if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?(?:;\d+(\.\d+)?,\d+(\.\d+)?)*$/)) {
|
if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?(?:;\d+(\.\d+)?,\d+(\.\d+)?)*$/)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -449,7 +457,7 @@ export class AddonQtypeDdMarkerQuestion {
|
||||||
* @param coordinates "x,y".
|
* @param coordinates "x,y".
|
||||||
* @return Coordinates to the point.
|
* @return Coordinates to the point.
|
||||||
*/
|
*/
|
||||||
parsePoint(coordinates: string): AddonQtypeDdMarkerQuestionPoint {
|
parsePoint(coordinates: string): CoreCoordinates {
|
||||||
const bits = coordinates.split(',');
|
const bits = coordinates.split(',');
|
||||||
if (bits.length !== 2) {
|
if (bits.length !== 2) {
|
||||||
throw coordinates + ' is not a valid point';
|
throw coordinates + ' is not a valid point';
|
||||||
|
@ -464,7 +472,7 @@ export class AddonQtypeDdMarkerQuestion {
|
||||||
* @param point Point coordinates.
|
* @param point Point coordinates.
|
||||||
* @return Converted point.
|
* @return Converted point.
|
||||||
*/
|
*/
|
||||||
makePointProportional(point: AddonQtypeDdMarkerQuestionPoint): AddonQtypeDdMarkerQuestionPoint {
|
makePointProportional(point: CoreCoordinates): CoreCoordinates {
|
||||||
return {
|
return {
|
||||||
x: Math.round(point.x * this.proportion),
|
x: Math.round(point.x * this.proportion),
|
||||||
y: Math.round(point.y * this.proportion),
|
y: Math.round(point.y * this.proportion),
|
||||||
|
@ -542,10 +550,10 @@ export class AddonQtypeDdMarkerQuestion {
|
||||||
* @return Coordinates.
|
* @return Coordinates.
|
||||||
*/
|
*/
|
||||||
getDragXY(dragItem: HTMLElement): number[] {
|
getDragXY(dragItem: HTMLElement): number[] {
|
||||||
const position = CoreDomUtils.getElementXY(dragItem, undefined, 'ddarea');
|
const position = this.getElementCoordinates(dragItem);
|
||||||
const bgImg = this.doc.bgImg();
|
const bgImg = this.doc.bgImg();
|
||||||
if (bgImg) {
|
if (bgImg) {
|
||||||
const bgImgXY = CoreDomUtils.getElementXY(bgImg, undefined, 'ddarea');
|
const bgImgXY = this.getElementCoordinates(bgImg);
|
||||||
|
|
||||||
position[0] -= bgImgXY[0];
|
position[0] -= bgImgXY[0];
|
||||||
position[1] -= bgImgXY[1];
|
position[1] -= bgImgXY[1];
|
||||||
|
@ -611,7 +619,7 @@ export class AddonQtypeDdMarkerQuestion {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.resizeListener = CoreDomUtils.onWindowResize(() => {
|
this.resizeListener = CoreDomUtils.onWindowResize(() => {
|
||||||
this.windowResized();
|
this.redrawDragsAndDrops();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -879,15 +887,6 @@ export class AddonQtypeDdMarkerQuestion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Window resized.
|
|
||||||
*/
|
|
||||||
async windowResized(): Promise<void> {
|
|
||||||
await CoreDomUtils.waitForResizeDone();
|
|
||||||
|
|
||||||
this.redrawDragsAndDrops();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -23,12 +23,6 @@ export class AddonQtypeDdMarkerGraphicsApi {
|
||||||
protected readonly NS = 'http://www.w3.org/2000/svg';
|
protected readonly NS = 'http://www.w3.org/2000/svg';
|
||||||
protected dropZone?: SVGSVGElement;
|
protected dropZone?: SVGSVGElement;
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the instance.
|
|
||||||
*
|
|
||||||
* @param instance Question instance.
|
|
||||||
* @param domUtils Dom Utils provider.
|
|
||||||
*/
|
|
||||||
constructor(protected instance: AddonQtypeDdMarkerQuestion) { }
|
constructor(protected instance: AddonQtypeDdMarkerQuestion) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,20 +54,20 @@ export class AddonQtypeDdMarkerGraphicsApi {
|
||||||
const bgImg = this.instance.doc?.bgImg();
|
const bgImg = this.instance.doc?.bgImg();
|
||||||
const dropZones = this.instance.doc?.topNode?.querySelector<HTMLElement>('div.ddarea div.dropzones');
|
const dropZones = this.instance.doc?.topNode?.querySelector<HTMLElement>('div.ddarea div.dropzones');
|
||||||
const markerTexts = this.instance.doc?.markerTexts();
|
const markerTexts = this.instance.doc?.markerTexts();
|
||||||
|
const ddArea = this.instance.doc?.topNode?.querySelector<HTMLElement>('.ddarea');
|
||||||
if (!bgImg || !dropZones || !markerTexts) {
|
if (!bgImg || !dropZones || !markerTexts || !ddArea) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = CoreDomUtils.getElementXY(bgImg, undefined, 'ddarea');
|
const position = CoreDomUtils.getRelativeElementPosition(bgImg, ddArea);
|
||||||
|
|
||||||
dropZones.style.left = position[0] + 'px';
|
dropZones.style.left = position.x + 'px';
|
||||||
dropZones.style.top = position[1] + 'px';
|
dropZones.style.top = position.y + 'px';
|
||||||
dropZones.style.width = bgImg.width + 'px';
|
dropZones.style.width = bgImg.width + 'px';
|
||||||
dropZones.style.height = bgImg.height + 'px';
|
dropZones.style.height = bgImg.height + 'px';
|
||||||
|
|
||||||
markerTexts.style.left = position[0] + 'px';
|
markerTexts.style.left = position.x + 'px';
|
||||||
markerTexts.style.top = position[1] + 'px';
|
markerTexts.style.top = position.y + 'px';
|
||||||
markerTexts.style.width = bgImg.width + 'px';
|
markerTexts.style.width = bgImg.width + 'px';
|
||||||
markerTexts.style.height = bgImg.height + 'px';
|
markerTexts.style.height = bgImg.height + 'px';
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,11 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreFormatTextDirective } from '@directives/format-text';
|
||||||
|
import { CoreCoordinates, CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
||||||
import { CoreEventObserver } from '@singletons/events';
|
import { CoreEventObserver } from '@singletons/events';
|
||||||
import { CoreLogger } from '@singletons/logger';
|
import { CoreLogger } from '@singletons/logger';
|
||||||
import { AddonModQuizDdwtosQuestionData } from '../component/ddwtos';
|
import { AddonModQuizDdwtosQuestionData } from '../component/ddwtos';
|
||||||
|
@ -26,7 +28,7 @@ export class AddonQtypeDdwtosQuestion {
|
||||||
|
|
||||||
protected logger: CoreLogger;
|
protected logger: CoreLogger;
|
||||||
protected nextDragItemNo = 1;
|
protected nextDragItemNo = 1;
|
||||||
protected selectors!: AddonQtypeDdwtosQuestionCSSSelectors; // Result of cssSelectors.
|
protected selectors = new AddonQtypeDdwtosQuestionCSSSelectors(); // Result of cssSelectors.
|
||||||
protected placed: {[no: number]: number} = {}; // Map that relates drag elements numbers with drop zones numbers.
|
protected placed: {[no: number]: number} = {}; // Map that relates drag elements numbers with drop zones numbers.
|
||||||
protected selected?: HTMLElement; // Selected element (being "dragged").
|
protected selected?: HTMLElement; // Selected element (being "dragged").
|
||||||
protected resizeListener?: CoreEventObserver;
|
protected resizeListener?: CoreEventObserver;
|
||||||
|
@ -80,8 +82,8 @@ export class AddonQtypeDdwtosQuestion {
|
||||||
* Invisible 'drag homes' are output in the question. These have the same properties as the drag items but are invisible.
|
* Invisible 'drag homes' are output in the question. These have the same properties as the drag items but are invisible.
|
||||||
* We clone these invisible elements to make the actual drag items.
|
* We clone these invisible elements to make the actual drag items.
|
||||||
*/
|
*/
|
||||||
cloneDragItems(): void {
|
async cloneDragItems(): Promise<void> {
|
||||||
const dragHomes = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.dragHomes()));
|
const dragHomes = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.dragHomes()));
|
||||||
for (let x = 0; x < dragHomes.length; x++) {
|
for (let x = 0; x < dragHomes.length; x++) {
|
||||||
this.cloneDragItemsForOneChoice(dragHomes[x]);
|
this.cloneDragItemsForOneChoice(dragHomes[x]);
|
||||||
}
|
}
|
||||||
|
@ -110,7 +112,7 @@ export class AddonQtypeDdwtosQuestion {
|
||||||
*/
|
*/
|
||||||
deselectDrags(): void {
|
deselectDrags(): void {
|
||||||
// Remove the selected class from all drags.
|
// Remove the selected class from all drags.
|
||||||
const drags = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.drags()));
|
const drags = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.drags()));
|
||||||
drags.forEach((drag) => {
|
drags.forEach((drag) => {
|
||||||
drag.classList.remove('selected');
|
drag.classList.remove('selected');
|
||||||
});
|
});
|
||||||
|
@ -192,19 +194,13 @@ export class AddonQtypeDdwtosQuestion {
|
||||||
* Initialize the question.
|
* Initialize the question.
|
||||||
*/
|
*/
|
||||||
async initializer(): Promise<void> {
|
async initializer(): Promise<void> {
|
||||||
this.selectors = new AddonQtypeDdwtosQuestionCSSSelectors();
|
const container = this.container.querySelector<HTMLElement>(this.selectors.topNode());
|
||||||
|
container?.classList.add(this.readOnly ? 'readonly' : 'notreadonly');
|
||||||
const container = <HTMLElement> this.container.querySelector(this.selectors.topNode());
|
|
||||||
if (this.readOnly) {
|
|
||||||
container.classList.add('readonly');
|
|
||||||
} else {
|
|
||||||
container.classList.add('notreadonly');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the elements to be ready.
|
// Wait for the elements to be ready.
|
||||||
await this.waitForReady();
|
await this.waitForReady();
|
||||||
|
|
||||||
this.setPaddingSizesAll();
|
await this.setPaddingSizesAll();
|
||||||
this.cloneDragItems();
|
this.cloneDragItems();
|
||||||
this.initialPlaceOfDragItems();
|
this.initialPlaceOfDragItems();
|
||||||
this.makeDropZones();
|
this.makeDropZones();
|
||||||
|
@ -212,7 +208,7 @@ export class AddonQtypeDdwtosQuestion {
|
||||||
this.positionDragItems();
|
this.positionDragItems();
|
||||||
|
|
||||||
this.resizeListener = CoreDomUtils.onWindowResize(() => {
|
this.resizeListener = CoreDomUtils.onWindowResize(() => {
|
||||||
this.windowResized();
|
this.positionDragItems();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,7 +216,7 @@ export class AddonQtypeDdwtosQuestion {
|
||||||
* Initialize drag items, putting them in their initial place.
|
* Initialize drag items, putting them in their initial place.
|
||||||
*/
|
*/
|
||||||
initialPlaceOfDragItems(): void {
|
initialPlaceOfDragItems(): void {
|
||||||
const drags = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.drags()));
|
const drags = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.drags()));
|
||||||
|
|
||||||
// Add the class 'unplaced' to all elements.
|
// Add the class 'unplaced' to all elements.
|
||||||
drags.forEach((drag) => {
|
drags.forEach((drag) => {
|
||||||
|
@ -292,15 +288,15 @@ export class AddonQtypeDdwtosQuestion {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create all the drop zones.
|
// Create all the drop zones.
|
||||||
const drops = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.drops()));
|
const drops = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.drops()));
|
||||||
drops.forEach((drop) => {
|
drops.forEach((drop) => {
|
||||||
this.makeDropZone(drop);
|
this.makeDropZone(drop);
|
||||||
});
|
});
|
||||||
|
|
||||||
// If home answer zone is clicked, return drag home.
|
// If home answer zone is clicked, return drag home.
|
||||||
const home = <HTMLElement> this.container.querySelector(this.selectors.topNode() + ' .answercontainer');
|
const home = this.container.querySelector<HTMLElement>(this.selectors.topNode() + ' .answercontainer');
|
||||||
|
|
||||||
home.addEventListener('click', () => {
|
home?.addEventListener('click', () => {
|
||||||
const drag = this.selected;
|
const drag = this.selected;
|
||||||
if (!drag) {
|
if (!drag) {
|
||||||
// No element selected, nothing to do.
|
// No element selected, nothing to do.
|
||||||
|
@ -379,33 +375,37 @@ export class AddonQtypeDdwtosQuestion {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let position;
|
|
||||||
|
|
||||||
const placeNo = this.placed[this.getNo(drag) ?? -1];
|
const placeNo = this.placed[this.getNo(drag) ?? -1];
|
||||||
|
const parent = this.container.querySelector<HTMLElement>('.addon-qtype-ddwtos-container');
|
||||||
|
if (!parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let position: CoreCoordinates | undefined;
|
||||||
|
|
||||||
if (!placeNo) {
|
if (!placeNo) {
|
||||||
// Not placed, put it in home zone.
|
// Not placed, put it in home zone.
|
||||||
const groupNo = this.getGroup(drag) ?? -1;
|
const groupNo = this.getGroup(drag) ?? -1;
|
||||||
const choiceNo = this.getChoice(drag) ?? -1;
|
const choiceNo = this.getChoice(drag) ?? -1;
|
||||||
|
const dragHome = this.container.querySelector<HTMLElement>(this.selectors.dragHome(groupNo, choiceNo));
|
||||||
position = CoreDomUtils.getElementXY(
|
if (dragHome) {
|
||||||
this.container,
|
position = CoreDomUtils.getRelativeElementPosition(dragHome, parent);
|
||||||
this.selectors.dragHome(groupNo, choiceNo),
|
}
|
||||||
'answercontainer',
|
|
||||||
);
|
|
||||||
drag.classList.add('unplaced');
|
|
||||||
} else {
|
} else {
|
||||||
// Get the drop zone position.
|
// Get the drop zone position.
|
||||||
position = CoreDomUtils.getElementXY(
|
const dropZone = this.container.querySelector<HTMLElement>(this.selectors.dropForPlace(placeNo));
|
||||||
this.container,
|
if (dropZone) {
|
||||||
this.selectors.dropForPlace(placeNo),
|
position = CoreDomUtils.getRelativeElementPosition(dropZone, parent);
|
||||||
'addon-qtype-ddwtos-container',
|
// Avoid the border.
|
||||||
);
|
position.x++;
|
||||||
drag.classList.remove('unplaced');
|
position.y++;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
drag.classList.toggle('unplaced', !placeNo);
|
||||||
|
|
||||||
if (position) {
|
if (position) {
|
||||||
drag.style.left = position[0] + 'px';
|
drag.style.left = position.x + 'px';
|
||||||
drag.style.top = position[1] + 'px';
|
drag.style.top = position.y + 'px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,36 +413,25 @@ export class AddonQtypeDdwtosQuestion {
|
||||||
* Postition, or reposition, all the drag items. They're placed in the right drop zone or in the home zone.
|
* Postition, or reposition, all the drag items. They're placed in the right drop zone or in the home zone.
|
||||||
*/
|
*/
|
||||||
positionDragItems(): void {
|
positionDragItems(): void {
|
||||||
const drags = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.drags()));
|
const drags = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.drags()));
|
||||||
drags.forEach((drag) => {
|
drags.forEach((drag) => {
|
||||||
this.positionDragItem(drag);
|
this.positionDragItem(drag);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for the drag items to have an offsetParent. For some reason it takes a while.
|
* Wait for the drag home items to be in DOM.
|
||||||
*
|
*
|
||||||
* @param retries Number of times this has been retried.
|
* @return Promise resolved when ready in the DOM.
|
||||||
* @return Promise resolved when ready or if it took too long to load.
|
|
||||||
*/
|
*/
|
||||||
protected async waitForReady(retries: number = 0): Promise<void> {
|
protected async waitForReady(): Promise<void> {
|
||||||
const drag = <HTMLElement | null> Array.from(this.container.querySelectorAll(this.selectors.drags()))[0];
|
await CoreDomUtils.waitToBeInDOM(this.container);
|
||||||
if (drag?.offsetParent || retries >= 10) {
|
|
||||||
// Ready or too many retries, stop.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deferred = CoreUtils.promiseDefer<void>();
|
await CoreComponentsRegistry.waitComponentsReady(this.container, 'core-format-text', CoreFormatTextDirective);
|
||||||
|
|
||||||
setTimeout(async () => {
|
const drag = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.dragHomes()))[0];
|
||||||
try {
|
|
||||||
await this.waitForReady(retries + 1);
|
|
||||||
} finally {
|
|
||||||
deferred.resolve();
|
|
||||||
}
|
|
||||||
}, 20);
|
|
||||||
|
|
||||||
return deferred.promise;
|
await CoreDomUtils.waitToBeInDOM(drag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -452,7 +441,7 @@ export class AddonQtypeDdwtosQuestion {
|
||||||
*/
|
*/
|
||||||
removeDragFromDrop(drag: HTMLElement): void {
|
removeDragFromDrop(drag: HTMLElement): void {
|
||||||
const placeNo = this.placed[this.getNo(drag) ?? -1];
|
const placeNo = this.placed[this.getNo(drag) ?? -1];
|
||||||
const drop = <HTMLElement> this.container.querySelector(this.selectors.dropForPlace(placeNo));
|
const drop = this.container.querySelector<HTMLElement>(this.selectors.dropForPlace(placeNo));
|
||||||
|
|
||||||
this.placeDragInDrop(null, drop);
|
this.placeDragInDrop(null, drop);
|
||||||
}
|
}
|
||||||
|
@ -473,9 +462,9 @@ export class AddonQtypeDdwtosQuestion {
|
||||||
/**
|
/**
|
||||||
* Set the padding size for all groups.
|
* Set the padding size for all groups.
|
||||||
*/
|
*/
|
||||||
setPaddingSizesAll(): void {
|
async setPaddingSizesAll(): Promise<void> {
|
||||||
for (let groupNo = 1; groupNo <= 8; groupNo++) {
|
for (let groupNo = 1; groupNo <= 8; groupNo++) {
|
||||||
this.setPaddingSizeForGroup(groupNo);
|
await this.setPaddingSizeForGroup(groupNo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -484,19 +473,25 @@ export class AddonQtypeDdwtosQuestion {
|
||||||
*
|
*
|
||||||
* @param groupNo Group number.
|
* @param groupNo Group number.
|
||||||
*/
|
*/
|
||||||
setPaddingSizeForGroup(groupNo: number): void {
|
async setPaddingSizeForGroup(groupNo: number): Promise<void> {
|
||||||
const groupItems = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.dragHomesGroup(groupNo)));
|
const groupItems = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.dragHomesGroup(groupNo)));
|
||||||
|
|
||||||
if (!groupItems.length) {
|
if (!groupItems.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await CoreDomUtils.waitToBeInDOM(groupItems[0]);
|
||||||
|
|
||||||
let maxWidth = 0;
|
let maxWidth = 0;
|
||||||
let maxHeight = 0;
|
let maxHeight = 0;
|
||||||
|
|
||||||
// Find max height and width.
|
// Find max height and width.
|
||||||
groupItems.forEach((item) => {
|
groupItems.forEach((item) => {
|
||||||
item.innerHTML = CoreTextUtils.decodeHTML(item.innerHTML);
|
item.innerHTML = CoreTextUtils.decodeHTML(item.innerHTML);
|
||||||
|
});
|
||||||
|
// Wait to render in order to calculate size.
|
||||||
|
await CoreUtils.nextTick();
|
||||||
|
|
||||||
|
groupItems.forEach((item) => {
|
||||||
maxWidth = Math.max(maxWidth, Math.ceil(item.offsetWidth));
|
maxWidth = Math.max(maxWidth, Math.ceil(item.offsetWidth));
|
||||||
maxHeight = Math.max(maxHeight, Math.ceil(item.offsetHeight));
|
maxHeight = Math.max(maxHeight, Math.ceil(item.offsetHeight));
|
||||||
});
|
});
|
||||||
|
@ -507,21 +502,12 @@ export class AddonQtypeDdwtosQuestion {
|
||||||
this.padToWidthHeight(item, maxWidth, maxHeight);
|
this.padToWidthHeight(item, maxWidth, maxHeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropsGroup = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.dropsGroup(groupNo)));
|
const dropsGroup = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.dropsGroup(groupNo)));
|
||||||
dropsGroup.forEach((item) => {
|
dropsGroup.forEach((item) => {
|
||||||
this.padToWidthHeight(item, maxWidth + 2, maxHeight + 2);
|
this.padToWidthHeight(item, maxWidth + 2, maxHeight + 2);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Window resized.
|
|
||||||
*/
|
|
||||||
async windowResized(): Promise<void> {
|
|
||||||
await CoreDomUtils.waitForResizeDone();
|
|
||||||
|
|
||||||
this.positionDragItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -103,7 +103,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
||||||
selector += '[slot="' + slot + '"]';
|
selector += '[slot="' + slot + '"]';
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttonsContainer = <HTMLElement> header.querySelector(selector);
|
const buttonsContainer = header.querySelector<HTMLIonButtonsElement>(selector);
|
||||||
if (buttonsContainer) {
|
if (buttonsContainer) {
|
||||||
this.mergeContextMenus(buttonsContainer);
|
this.mergeContextMenus(buttonsContainer);
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
||||||
*
|
*
|
||||||
* @param buttonsContainer The container where the buttons will be moved.
|
* @param buttonsContainer The container where the buttons will be moved.
|
||||||
*/
|
*/
|
||||||
protected mergeContextMenus(buttonsContainer: HTMLElement): void {
|
protected mergeContextMenus(buttonsContainer: HTMLIonButtonsElement): void {
|
||||||
// Check if both button containers have a context menu.
|
// Check if both button containers have a context menu.
|
||||||
const secondaryContextMenu = this.element.querySelector('core-context-menu');
|
const secondaryContextMenu = this.element.querySelector('core-context-menu');
|
||||||
if (!secondaryContextMenu) {
|
if (!secondaryContextMenu) {
|
||||||
|
@ -194,60 +194,26 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* Search the ion-header where the buttons should be added.
|
* Search the ion-header where the buttons should be added.
|
||||||
*
|
*
|
||||||
* @param retries Number of retries so far.
|
|
||||||
* @return Promise resolved with the header element.
|
* @return Promise resolved with the header element.
|
||||||
*/
|
*/
|
||||||
protected async searchHeader(retries: number = 0): Promise<HTMLElement> {
|
protected async searchHeader(): Promise<HTMLIonHeaderElement> {
|
||||||
|
await CoreDomUtils.waitToBeInDOM(this.element);
|
||||||
|
|
||||||
let parentPage: HTMLElement | null = this.element;
|
let parentPage: HTMLElement | null = this.element;
|
||||||
|
while (parentPage && parentPage.parentElement) {
|
||||||
while (parentPage) {
|
|
||||||
if (!parentPage.parentElement) {
|
|
||||||
// No parent, stop.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the next parent page.
|
// Get the next parent page.
|
||||||
parentPage = parentPage.parentElement.closest('.ion-page');
|
parentPage = parentPage.parentElement.closest('.ion-page');
|
||||||
if (parentPage) {
|
|
||||||
// Check if the page has a header. If it doesn't, search the next parent page.
|
// Check if the page has a header. If it doesn't, search the next parent page.
|
||||||
const header = this.searchHeaderInPage(parentPage);
|
const header = parentPage?.querySelector<HTMLIonHeaderElement>(':scope > ion-header');
|
||||||
if (header && getComputedStyle(header, null).display != 'none') {
|
if (header && getComputedStyle(header).display !== 'none') {
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Header not found.
|
// Header not found, reject.
|
||||||
if (retries < 5) {
|
|
||||||
// If the component or any of its parent is inside a ng-content or similar it can be detached when it's initialized.
|
|
||||||
// Try again after a while.
|
|
||||||
return new Promise((resolve, reject): void => {
|
|
||||||
setTimeout(() => {
|
|
||||||
// eslint-disable-next-line promise/catch-or-return
|
|
||||||
this.searchHeader(retries + 1).then(resolve, reject);
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// We've waited enough time, reject.
|
|
||||||
throw Error('Header not found.');
|
throw Error('Header not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Search ion-header inside a page. The header should be a direct child.
|
|
||||||
*
|
|
||||||
* @param page Page to search in.
|
|
||||||
* @return Header element. Undefined if not found.
|
|
||||||
*/
|
|
||||||
protected searchHeaderInPage(page: HTMLElement): HTMLElement | undefined {
|
|
||||||
for (let i = 0; i < page.children.length; i++) {
|
|
||||||
const child = page.children[i];
|
|
||||||
if (child.tagName == 'ION-HEADER') {
|
|
||||||
return <HTMLElement> child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show or hide all the elements.
|
* Show or hide all the elements.
|
||||||
*/
|
*/
|
||||||
|
@ -280,7 +246,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component destroyed.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
// This component was destroyed, remove all the buttons that were moved.
|
// This component was destroyed, remove all the buttons that were moved.
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
<ion-button fill="clear" [attr.aria-label]="label | translate" core-suppress-events (onClick)="toggle($event)">
|
<ion-button fill="clear" [attr.aria-label]="label | translate" core-suppress-events (onClick)="toggle($event)"
|
||||||
<ion-icon [name]="iconName" slot="icon-only" aria-hidden="true"></ion-icon>
|
(mousedown)="doNotBlur($event)" (keydown)="doNotBlur($event)" (keyup)="toggle($event)">
|
||||||
|
<ion-icon [name]=" iconName" slot="icon-only" aria-hidden="true">
|
||||||
|
</ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
|
@ -44,12 +44,11 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
|
||||||
@Input() initialShown?: boolean | string; // Whether the password should be shown at start.
|
@Input() initialShown?: boolean | string; // Whether the password should be shown at start.
|
||||||
@ContentChild(IonInput) ionInput?: IonInput;
|
@ContentChild(IonInput) ionInput?: IonInput;
|
||||||
|
|
||||||
shown!: boolean; // Whether the password is shown.
|
shown = false; // Whether the password is shown.
|
||||||
label!: string; // Label for the button to show/hide.
|
label = ''; // Label for the button to show/hide.
|
||||||
iconName!: string; // Name of the icon of the button to show/hide.
|
iconName = ''; // Name of the icon of the button to show/hide.
|
||||||
selector = ''; // Selector to identify the input.
|
|
||||||
|
|
||||||
protected input?: HTMLInputElement | null; // Input affected.
|
protected input?: HTMLInputElement; // Input affected.
|
||||||
protected element: HTMLElement; // Current element.
|
protected element: HTMLElement; // Current element.
|
||||||
|
|
||||||
constructor(element: ElementRef) {
|
constructor(element: ElementRef) {
|
||||||
|
@ -57,38 +56,32 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.shown = CoreUtils.isTrueOrOne(this.initialShown);
|
this.shown = CoreUtils.isTrueOrOne(this.initialShown);
|
||||||
this.selector = 'input[name="' + this.name + '"]';
|
|
||||||
this.setData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View has been initialized.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngAfterViewInit(): void {
|
async ngAfterViewInit(): Promise<void> {
|
||||||
this.searchInput();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search the input to show/hide.
|
|
||||||
*/
|
|
||||||
protected async searchInput(): Promise<void> {
|
|
||||||
if (this.ionInput) {
|
if (this.ionInput) {
|
||||||
// It's an ion-input, use it to get the native element.
|
// It's an ion-input, use it to get the native element.
|
||||||
this.input = await this.ionInput.getInputElement();
|
this.input = await this.ionInput.getInputElement();
|
||||||
|
this.setData(this.input);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search the input.
|
// Search the input.
|
||||||
this.input = <HTMLInputElement> this.element.querySelector(this.selector);
|
this.input = this.element.querySelector<HTMLInputElement>('input[name="' + this.name + '"]') ?? undefined;
|
||||||
|
|
||||||
if (this.input) {
|
if (!this.input) {
|
||||||
// Input found. Set the right type.
|
return;
|
||||||
this.input.type = this.shown ? 'text' : 'password';
|
}
|
||||||
|
|
||||||
|
this.setData(this.input);
|
||||||
|
|
||||||
// By default, don't autocapitalize and autocorrect.
|
// By default, don't autocapitalize and autocorrect.
|
||||||
if (!this.input.getAttribute('autocorrect')) {
|
if (!this.input.getAttribute('autocorrect')) {
|
||||||
|
@ -98,17 +91,16 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
|
||||||
this.input.setAttribute('autocapitalize', 'none');
|
this.input.setAttribute('autocapitalize', 'none');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set label, icon name and input type.
|
* Set label, icon name and input type.
|
||||||
|
*
|
||||||
|
* @param input The input element.
|
||||||
*/
|
*/
|
||||||
protected setData(): void {
|
protected setData(input: HTMLInputElement): void {
|
||||||
this.label = this.shown ? 'core.hide' : 'core.show';
|
this.label = this.shown ? 'core.hide' : 'core.show';
|
||||||
this.iconName = this.shown ? 'fas-eye-slash' : 'fas-eye';
|
this.iconName = this.shown ? 'fas-eye-slash' : 'fas-eye';
|
||||||
if (this.input) {
|
input.type = this.shown ? 'text' : 'password';
|
||||||
this.input.type = this.shown ? 'text' : 'password';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -117,20 +109,49 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
|
||||||
* @param event The mouse event.
|
* @param event The mouse event.
|
||||||
*/
|
*/
|
||||||
toggle(event: Event): void {
|
toggle(event: Event): void {
|
||||||
|
if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const isFocused = document.activeElement === this.input;
|
const isFocused = document.activeElement === this.input;
|
||||||
|
|
||||||
this.shown = !this.shown;
|
this.shown = !this.shown;
|
||||||
this.setData();
|
|
||||||
|
|
||||||
if (isFocused && CoreApp.isAndroid()) {
|
if (!this.input) {
|
||||||
// In Android, the keyboard is closed when the input type changes. Focus it again.
|
return;
|
||||||
setTimeout(() => {
|
|
||||||
CoreDomUtils.focusElement(this.input!);
|
|
||||||
}, 400);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setData(this.input);
|
||||||
|
// In Android, the keyboard is closed when the input type changes. Focus it again.
|
||||||
|
if (isFocused && CoreApp.isAndroid()) {
|
||||||
|
CoreDomUtils.focusElement(this.input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not loose focus.
|
||||||
|
*
|
||||||
|
* @param event The mouse event.
|
||||||
|
*/
|
||||||
|
doNotBlur(event: Event): void {
|
||||||
|
if (event.type == 'keydown' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if Space or Enter have been pressed.
|
||||||
|
*
|
||||||
|
* @param event Keyboard Event.
|
||||||
|
* @returns Wether space or enter have been pressed.
|
||||||
|
*/
|
||||||
|
protected isValidKeyboardKey(event: KeyboardEvent): boolean {
|
||||||
|
return event.key == ' ' || event.key == 'Enter';
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class CoreAutoFocusDirective implements AfterViewInit {
|
||||||
|
|
||||||
@Input('core-auto-focus') autoFocus: boolean | string = true;
|
@Input('core-auto-focus') autoFocus: boolean | string = true;
|
||||||
|
|
||||||
protected element: HTMLElement;
|
protected element: HTMLIonInputElement | HTMLIonTextareaElement | HTMLIonSearchbarElement | HTMLElement;
|
||||||
|
|
||||||
constructor(element: ElementRef) {
|
constructor(element: ElementRef) {
|
||||||
this.element = element.nativeElement;
|
this.element = element.nativeElement;
|
||||||
|
@ -41,51 +41,27 @@ export class CoreAutoFocusDirective implements AfterViewInit {
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngAfterViewInit(): void {
|
async ngAfterViewInit(): Promise<void> {
|
||||||
if (CoreUtils.isFalseOrZero(this.autoFocus)) {
|
if (CoreUtils.isFalseOrZero(this.autoFocus)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setFocus();
|
await CoreDomUtils.waitToBeInDOM(this.element);
|
||||||
|
|
||||||
|
let focusElement = this.element;
|
||||||
|
|
||||||
|
if ('getInputElement' in focusElement) {
|
||||||
|
// If it's an Ionic element get the right input to use.
|
||||||
|
focusElement.componentOnReady && await focusElement.componentOnReady();
|
||||||
|
focusElement = await focusElement.getInputElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
if (!focusElement) {
|
||||||
* Function to focus the element.
|
|
||||||
*
|
|
||||||
* @param retries Internal param to stop retrying on 0.
|
|
||||||
*/
|
|
||||||
protected setFocus(retries = 10): void {
|
|
||||||
if (retries == 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait a bit to make sure the view is loaded.
|
CoreDomUtils.focusElement(focusElement);
|
||||||
setTimeout(() => {
|
|
||||||
// If it's a ion-input or ion-textarea, search the right input to use.
|
|
||||||
let element: HTMLElement | null = null;
|
|
||||||
|
|
||||||
if (this.element.tagName == 'ION-INPUT') {
|
|
||||||
element = this.element.querySelector('input');
|
|
||||||
} else if (this.element.tagName == 'ION-TEXTAREA') {
|
|
||||||
element = this.element.querySelector('textarea');
|
|
||||||
} else {
|
|
||||||
element = this.element;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!element) {
|
|
||||||
this.setFocus(retries - 1);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CoreDomUtils.focusElement(element);
|
|
||||||
|
|
||||||
if (element != document.activeElement) {
|
|
||||||
this.setFocus(retries - 1);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,10 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
|
||||||
|
|
||||||
await this.calculateHeight();
|
await this.calculateHeight();
|
||||||
|
|
||||||
|
CoreDomUtils.onElementSlot(this.element, () => {
|
||||||
|
this.calculateHeight();
|
||||||
|
});
|
||||||
|
|
||||||
this.listenScrollEvents();
|
this.listenScrollEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +150,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
|
||||||
? this.finalHeight
|
? this.finalHeight
|
||||||
: this.initialHeight;
|
: this.initialHeight;
|
||||||
|
|
||||||
this.setBarHeight(newHeight); }
|
this.setBarHeight(newHeight);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.resizeListener = CoreDomUtils.onWindowResize(() => {
|
this.resizeListener = CoreDomUtils.onWindowResize(() => {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { CoreCancellablePromise } from '@classes/cancellable-promise';
|
import { CoreCancellablePromise } from '@classes/cancellable-promise';
|
||||||
import { CoreLoadingComponent } from '@components/loading/loading';
|
import { CoreLoadingComponent } from '@components/loading/loading';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
||||||
import { CoreEventObserver } from '@singletons/events';
|
import { CoreEventObserver } from '@singletons/events';
|
||||||
|
@ -50,11 +51,14 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
|
||||||
protected expandedHeight = 0;
|
protected expandedHeight = 0;
|
||||||
protected resizeListener?: CoreEventObserver;
|
protected resizeListener?: CoreEventObserver;
|
||||||
protected domPromise?: CoreCancellablePromise<void>;
|
protected domPromise?: CoreCancellablePromise<void>;
|
||||||
|
protected uniqueId: string;
|
||||||
|
|
||||||
constructor(el: ElementRef<HTMLElement>) {
|
constructor(el: ElementRef<HTMLElement>) {
|
||||||
this.element = el.nativeElement;
|
this.element = el.nativeElement;
|
||||||
|
|
||||||
this.element.addEventListener('click', this.elementClicked.bind(this));
|
this.element.addEventListener('click', this.elementClicked.bind(this));
|
||||||
|
this.uniqueId = 'collapsible-item-' + CoreUtils.getUniqueId('CoreCollapsibleItemDirective');
|
||||||
|
this.element.id = this.uniqueId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -153,6 +157,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
|
||||||
const toggleButton = document.createElement('ion-button');
|
const toggleButton = document.createElement('ion-button');
|
||||||
toggleButton.classList.add('collapsible-toggle');
|
toggleButton.classList.add('collapsible-toggle');
|
||||||
toggleButton.setAttribute('fill', 'clear');
|
toggleButton.setAttribute('fill', 'clear');
|
||||||
|
toggleButton.setAttribute('aria-controls', this.uniqueId);
|
||||||
|
|
||||||
const toggleText = document.createElement('span');
|
const toggleText = document.createElement('span');
|
||||||
toggleText.classList.add('collapsible-toggle-text');
|
toggleText.classList.add('collapsible-toggle-text');
|
||||||
|
@ -163,7 +168,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
|
||||||
expandArrow.classList.add('collapsible-toggle-arrow');
|
expandArrow.classList.add('collapsible-toggle-arrow');
|
||||||
toggleButton.appendChild(expandArrow);
|
toggleButton.appendChild(expandArrow);
|
||||||
|
|
||||||
this.element.appendChild(toggleButton);
|
this.element.append(toggleButton);
|
||||||
|
|
||||||
this.toggleExpand(this.expanded);
|
this.toggleExpand(this.expanded);
|
||||||
}
|
}
|
||||||
|
@ -194,6 +199,9 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
|
||||||
expand = !this.expanded;
|
expand = !this.expanded;
|
||||||
}
|
}
|
||||||
this.expanded = expand;
|
this.expanded = expand;
|
||||||
|
|
||||||
|
// Reset scroll inside the element to show always the top part.
|
||||||
|
this.element.scrollTo(0, 0);
|
||||||
this.element.classList.toggle('collapsible-collapsed', !expand);
|
this.element.classList.toggle('collapsible-collapsed', !expand);
|
||||||
this.setHeight(!expand ? this.maxHeight: undefined);
|
this.setHeight(!expand ? this.maxHeight: undefined);
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,8 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core';
|
import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreCancellablePromise } from '@classes/cancellable-promise';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directive to move ion-fab components as direct children of the nearest ion-content.
|
* Directive to move ion-fab components as direct children of the nearest ion-content.
|
||||||
|
@ -30,6 +31,7 @@ export class CoreFabDirective implements OnInit, OnDestroy {
|
||||||
protected element: HTMLElement;
|
protected element: HTMLElement;
|
||||||
protected content?: HTMLIonContentElement | null;
|
protected content?: HTMLIonContentElement | null;
|
||||||
protected initialPaddingBottom = 0;
|
protected initialPaddingBottom = 0;
|
||||||
|
protected domPromise?: CoreCancellablePromise<void>;
|
||||||
|
|
||||||
constructor(el: ElementRef) {
|
constructor(el: ElementRef) {
|
||||||
this.element = el.nativeElement;
|
this.element = el.nativeElement;
|
||||||
|
@ -39,15 +41,31 @@ export class CoreFabDirective implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(retries = 3): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
this.content = this.element.closest('ion-content');
|
this.domPromise = CoreDomUtils.waitToBeInDOM(this.element);
|
||||||
if (!this.content) {
|
await this.domPromise;
|
||||||
if(retries > 0) {
|
|
||||||
await CoreUtils.nextTicks(50);
|
|
||||||
|
|
||||||
this.ngOnInit(retries - 1);
|
this.content = this.element.closest('ion-content');
|
||||||
|
|
||||||
|
if (!this.content) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add space at the bottom to let the user see the whole content.
|
||||||
|
this.initialPaddingBottom = parseFloat(this.content.style.getPropertyValue('--padding-bottom') || '0');
|
||||||
|
|
||||||
|
await this.calculatePlace();
|
||||||
|
|
||||||
|
CoreDomUtils.onElementSlot(this.element, () => {
|
||||||
|
this.calculatePlace();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the height of the footer.
|
||||||
|
*/
|
||||||
|
protected async calculatePlace(): Promise<void> {
|
||||||
|
if (!this.content) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,8 +76,6 @@ export class CoreFabDirective implements OnInit, OnDestroy {
|
||||||
this.content.appendChild(this.element);
|
this.content.appendChild(this.element);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add space at the bottom to let the user see the whole content.
|
|
||||||
this.initialPaddingBottom = parseFloat(this.content.style.getPropertyValue('--padding-bottom') || '0');
|
|
||||||
this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + initialHeight + 'px');
|
this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + initialHeight + 'px');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +86,7 @@ export class CoreFabDirective implements OnInit, OnDestroy {
|
||||||
if (this.content) {
|
if (this.content) {
|
||||||
this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px');
|
this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px');
|
||||||
}
|
}
|
||||||
|
this.domPromise?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,11 +142,13 @@ export class CoreLinkDirective implements OnInit {
|
||||||
if (href.charAt(0) == '#') {
|
if (href.charAt(0) == '#') {
|
||||||
// Look for id or name.
|
// Look for id or name.
|
||||||
href = href.substring(1);
|
href = href.substring(1);
|
||||||
CoreDomUtils.scrollToElementBySelector(
|
const container = this.element.closest('ion-content');
|
||||||
this.element.closest('ion-content'),
|
if (container) {
|
||||||
this.content,
|
CoreDomUtils.scrollViewToElement(
|
||||||
|
container,
|
||||||
`#${href}, [name='${href}']`,
|
`#${href}, [name='${href}']`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import {
|
||||||
CoreCourseProvider,
|
CoreCourseProvider,
|
||||||
} from '@features/course/services/course';
|
} from '@features/course/services/course';
|
||||||
import {
|
import {
|
||||||
|
CoreCourseHelper,
|
||||||
CoreCourseSection,
|
CoreCourseSection,
|
||||||
} from '@features/course/services/course-helper';
|
} from '@features/course/services/course-helper';
|
||||||
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||||
|
@ -444,7 +445,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
await CoreCourseModuleDelegate.getModuleDataFor(module.modname, module, this.course.id);
|
await CoreCourseModuleDelegate.getModuleDataFor(module.modname, module, this.course.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (module.uservisible !== false && module.handlerData?.action) {
|
if (CoreCourseHelper.canUserViewModule(module, section) && module.handlerData?.action) {
|
||||||
module.handlerData.action(data.event, module, module.course);
|
module.handlerData.action(data.event, module, module.course);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -491,9 +492,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
// Scroll to module if needed. Give more priority to the input.
|
// Scroll to module if needed. Give more priority to the input.
|
||||||
const moduleIdToScroll = this.moduleId && previousValue === undefined ? this.moduleId : moduleId;
|
const moduleIdToScroll = this.moduleId && previousValue === undefined ? this.moduleId : moduleId;
|
||||||
if (moduleIdToScroll) {
|
if (moduleIdToScroll) {
|
||||||
setTimeout(() => {
|
|
||||||
this.scrollToModule(moduleIdToScroll);
|
this.scrollToModule(moduleIdToScroll);
|
||||||
}, 200);
|
|
||||||
} else {
|
} else {
|
||||||
this.content.scrollToTop(0);
|
this.content.scrollToTop(0);
|
||||||
}
|
}
|
||||||
|
@ -512,9 +511,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
* @param moduleId Module ID.
|
* @param moduleId Module ID.
|
||||||
*/
|
*/
|
||||||
protected scrollToModule(moduleId: number): void {
|
protected scrollToModule(moduleId: number): void {
|
||||||
CoreDomUtils.scrollToElementBySelector(
|
CoreDomUtils.scrollViewToElement(
|
||||||
this.elementRef.nativeElement,
|
this.elementRef.nativeElement,
|
||||||
this.content,
|
|
||||||
'#core-course-module-' + moduleId,
|
'#core-course-module-' + moduleId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -574,7 +572,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
modulesLoaded += sections[i].modules.reduce((total, module) => module.visibleoncoursepage !== 0 ? total + 1 : total, 0);
|
modulesLoaded += sections[i].modules.reduce((total, module) =>
|
||||||
|
!CoreCourseHelper.isModuleStealth(module, sections[i]) ? total + 1 : total, 0);
|
||||||
|
|
||||||
if (modulesLoaded >= CoreCourseFormatComponent.LOAD_MORE_ACTIVITIES) {
|
if (modulesLoaded >= CoreCourseFormatComponent.LOAD_MORE_ACTIVITIES) {
|
||||||
break;
|
break;
|
||||||
|
@ -632,8 +631,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
* @return Whether the section can be viewed.
|
* @return Whether the section can be viewed.
|
||||||
*/
|
*/
|
||||||
canViewSection(section: CoreCourseSection): boolean {
|
canViewSection(section: CoreCourseSection): boolean {
|
||||||
return section.uservisible !== false && !section.hiddenbynumsections &&
|
return CoreCourseHelper.canUserViewSection(section) && !CoreCourseHelper.isSectionStealth(section);
|
||||||
section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,16 +12,15 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
|
import { Component, ElementRef, Input, OnInit } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
CoreCourseModuleCompletionStatus,
|
CoreCourseModuleCompletionStatus,
|
||||||
CoreCourseModuleCompletionTracking,
|
CoreCourseModuleCompletionTracking,
|
||||||
CoreCourseProvider,
|
CoreCourseProvider,
|
||||||
} from '@features/course/services/course';
|
} from '@features/course/services/course';
|
||||||
import { CoreCourseSection } from '@features/course/services/course-helper';
|
import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper';
|
||||||
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||||
import { IonContent } from '@ionic/angular';
|
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { ModalController } from '@singletons';
|
import { ModalController } from '@singletons';
|
||||||
|
|
||||||
|
@ -35,8 +34,6 @@ import { ModalController } from '@singletons';
|
||||||
})
|
})
|
||||||
export class CoreCourseCourseIndexComponent implements OnInit {
|
export class CoreCourseCourseIndexComponent implements OnInit {
|
||||||
|
|
||||||
@ViewChild(IonContent) content?: IonContent;
|
|
||||||
|
|
||||||
@Input() sections: CoreCourseSection[] = [];
|
@Input() sections: CoreCourseSection[] = [];
|
||||||
@Input() selectedId?: number;
|
@Input() selectedId?: number;
|
||||||
@Input() course?: CoreCourseAnyCourseData;
|
@Input() course?: CoreCourseAnyCourseData;
|
||||||
|
@ -77,11 +74,10 @@ export class CoreCourseCourseIndexComponent implements OnInit {
|
||||||
|
|
||||||
// Clone sections to add information.
|
// Clone sections to add information.
|
||||||
this.sectionsToRender = this.sections
|
this.sectionsToRender = this.sections
|
||||||
.filter((section) => !section.hiddenbynumsections &&
|
.filter((section) => !CoreCourseHelper.isSectionStealth(section))
|
||||||
section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID)
|
|
||||||
.map((section) => {
|
.map((section) => {
|
||||||
const modules = section.modules
|
const modules = section.modules
|
||||||
.filter((module) => module.visibleoncoursepage !== 0 && !module.noviewlink)
|
.filter((module) => !CoreCourseHelper.isModuleStealth(module, section) && !module.noviewlink)
|
||||||
.map((module) => {
|
.map((module) => {
|
||||||
const completionStatus = !completionEnabled || module.completiondata === undefined ||
|
const completionStatus = !completionEnabled || module.completiondata === undefined ||
|
||||||
module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE
|
module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE
|
||||||
|
@ -93,7 +89,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
|
||||||
name: module.name,
|
name: module.name,
|
||||||
course: module.course,
|
course: module.course,
|
||||||
visible: !!module.visible,
|
visible: !!module.visible,
|
||||||
uservisible: !!module.uservisible,
|
uservisible: CoreCourseHelper.canUserViewModule(module, section),
|
||||||
completionStatus,
|
completionStatus,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -103,7 +99,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
|
||||||
name: section.name,
|
name: section.name,
|
||||||
availabilityinfo: !!section.availabilityinfo,
|
availabilityinfo: !!section.availabilityinfo,
|
||||||
visible: !!section.visible,
|
visible: !!section.visible,
|
||||||
uservisible: section.uservisible !== false,
|
uservisible: CoreCourseHelper.canUserViewSection(section),
|
||||||
expanded: section.id === this.selectedId,
|
expanded: section.id === this.selectedId,
|
||||||
highlighted: currentSectionData.section.id === section.id,
|
highlighted: currentSectionData.section.id === section.id,
|
||||||
hasVisibleModules: modules.length > 0,
|
hasVisibleModules: modules.length > 0,
|
||||||
|
@ -113,13 +109,10 @@ export class CoreCourseCourseIndexComponent implements OnInit {
|
||||||
|
|
||||||
this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
|
this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
|
||||||
|
|
||||||
setTimeout(() => {
|
CoreDomUtils.scrollViewToElement(
|
||||||
CoreDomUtils.scrollToElementBySelector(
|
|
||||||
this.elementRef.nativeElement,
|
this.elementRef.nativeElement,
|
||||||
this.content,
|
|
||||||
'.item.item-current',
|
'.item.item-current',
|
||||||
);
|
);
|
||||||
}, 300);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-chip>
|
</ion-chip>
|
||||||
|
|
||||||
<ion-chip *ngIf="rule.statusincomplete" color="dark" role="listitem" [attr.aria-label]="rule.accessibleDescription"
|
<ion-chip *ngIf="rule.statusincomplete" color="secondary" role="listitem" [attr.aria-label]="rule.accessibleDescription"
|
||||||
class="completioninfo completion_incomplete">
|
class="completioninfo completion_incomplete">
|
||||||
<ion-icon name="fas-edit" [attr.aria-label]="'core.course.completion_automatic:todo' | translate "></ion-icon>
|
<ion-icon name="fas-edit" [attr.aria-label]="'core.course.completion_automatic:todo' | translate "></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="mode == 'basic' && completion.istrackeduser">
|
<ng-container *ngIf="mode == 'basic' && completion.istrackeduser">
|
||||||
<ion-chip class="completioninfo completion_incomplete" *ngIf="completionStatus === 0" color="dark">
|
<ion-chip class="completioninfo completion_incomplete" *ngIf="completionStatus === 0" color="secondary">
|
||||||
<ion-icon name="fas-edit" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-edit" aria-hidden="true"></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
{{ 'core.course.todo' | translate }}
|
{{ 'core.course.todo' | translate }}
|
||||||
|
|
|
@ -13,8 +13,8 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { CoreCourse, CoreCourseProvider, CoreCourseWSSection } from '@features/course/services/course';
|
import { CoreCourse, CoreCourseWSSection } from '@features/course/services/course';
|
||||||
import { CoreCourseModuleData } from '@features/course/services/course-helper';
|
import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
|
||||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||||
import { IonContent } from '@ionic/angular';
|
import { IonContent } from '@ionic/angular';
|
||||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||||
|
@ -177,7 +177,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
|
||||||
* @return Wether the module is available to the user or not.
|
* @return Wether the module is available to the user or not.
|
||||||
*/
|
*/
|
||||||
protected async isModuleAvailable(module: CoreCourseModuleData): Promise<boolean> {
|
protected async isModuleAvailable(module: CoreCourseModuleData): Promise<boolean> {
|
||||||
return CoreCourse.instance.moduleHasView(module);
|
return !CoreCourseHelper.isModuleStealth(module) && CoreCourse.instance.moduleHasView(module);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -187,7 +187,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
|
||||||
* @return Wether the module is available to the user or not.
|
* @return Wether the module is available to the user or not.
|
||||||
*/
|
*/
|
||||||
protected isSectionAvailable(section: CoreCourseWSSection): boolean {
|
protected isSectionAvailable(section: CoreCourseWSSection): boolean {
|
||||||
return section.uservisible !== false && section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
return CoreCourseHelper.canUserViewSection(section) && !CoreCourseHelper.isSectionStealth(section);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -223,7 +223,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
|
||||||
animationDirection: next ? 'forward' : 'back',
|
animationDirection: next ? 'forward' : 'back',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (module.uservisible === false) {
|
if (!CoreCourseHelper.canUserViewModule(module)) {
|
||||||
const section = next ? this.nextModuleSection : this.previousModuleSection;
|
const section = next ? this.nextModuleSection : this.previousModuleSection;
|
||||||
options.params = {
|
options.params = {
|
||||||
module,
|
module,
|
||||||
|
|
|
@ -35,10 +35,10 @@
|
||||||
</ion-chip>
|
</ion-chip>
|
||||||
|
|
||||||
<!-- Hidden badges -->
|
<!-- Hidden badges -->
|
||||||
<ion-badge color="warning" *ngIf="module.visible === 0">
|
<ion-badge color="warning" *ngIf="module.visible === 0" class="ion-text-wrap">
|
||||||
{{ 'core.course.hiddenfromstudents' | translate }}
|
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||||
</ion-badge>
|
</ion-badge>
|
||||||
<ion-badge color="warning" *ngIf="module.visible !== 0 && module.isStealth">
|
<ion-badge color="warning" *ngIf="module.visible !== 0 && module.isStealth" class="ion-text-wrap">
|
||||||
{{ 'core.course.hiddenoncoursepage' | translate }}
|
{{ 'core.course.hiddenoncoursepage' | translate }}
|
||||||
</ion-badge>
|
</ion-badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
CoreCourseModuleData,
|
CoreCourseModuleData,
|
||||||
CoreCourseModuleCompletionData,
|
CoreCourseModuleCompletionData,
|
||||||
CoreCourseSection,
|
CoreCourseSection,
|
||||||
|
CoreCourseHelper,
|
||||||
} from '@features/course/services/course-helper';
|
} from '@features/course/services/course-helper';
|
||||||
import { CoreCourse, CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking } from '@features/course/services/course';
|
import { CoreCourse, CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking } from '@features/course/services/course';
|
||||||
import { CoreCourseModuleDelegate, CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate';
|
import { CoreCourseModuleDelegate, CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate';
|
||||||
|
@ -166,7 +167,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
||||||
* @param event Click event.
|
* @param event Click event.
|
||||||
*/
|
*/
|
||||||
moduleClicked(event: Event): void {
|
moduleClicked(event: Event): void {
|
||||||
if (this.module.uservisible !== false && this.module.handlerData?.action) {
|
if (CoreCourseHelper.canUserViewModule(this.module, this.section) && this.module.handlerData?.action) {
|
||||||
this.module.handlerData.action(event, this.module, this.module.course);
|
this.module.handlerData.action(event, this.module, this.module.course);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ export class CoreCourseListModTypePage implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
section.modules = section.modules.filter((mod) => {
|
section.modules = section.modules.filter((mod) => {
|
||||||
if (mod.uservisible === false || !CoreCourse.moduleHasView(mod)) {
|
if (!CoreCourseHelper.canUserViewModule(mod, section) || !CoreCourse.moduleHasView(mod)) {
|
||||||
// Ignore this module.
|
// Ignore this module.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,10 +33,10 @@
|
||||||
</ion-chip>
|
</ion-chip>
|
||||||
|
|
||||||
<!-- Hidden badges -->
|
<!-- Hidden badges -->
|
||||||
<ion-badge color="warning" *ngIf="module.visible === 0">
|
<ion-badge color="warning" *ngIf="module.visible === 0" class="ion-text-wrap">
|
||||||
{{ 'core.course.hiddenfromstudents' | translate }}
|
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||||
</ion-badge>
|
</ion-badge>
|
||||||
<ion-badge color="warning" *ngIf="module.visible !== 0 && module.isStealth">
|
<ion-badge color="warning" *ngIf="module.visible !== 0 && module.isStealth" class="ion-text-wrap">
|
||||||
{{ 'core.course.hiddenoncoursepage' | translate }}
|
{{ 'core.course.hiddenoncoursepage' | translate }}
|
||||||
</ion-badge>
|
</ion-badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -198,7 +198,7 @@ export class CoreCourseHelperProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the module is stealth.
|
// Check if the module is stealth.
|
||||||
module.isStealth = module.visibleoncoursepage === 0 || (!!module.visible && !section.visible);
|
module.isStealth = CoreCourseHelper.isModuleStealth(module, section);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return section;
|
return section;
|
||||||
|
@ -208,6 +208,50 @@ export class CoreCourseHelperProvider {
|
||||||
return { hasContent, sections: formattedSections };
|
return { hasContent, sections: formattedSections };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module is stealth.
|
||||||
|
*
|
||||||
|
* @param module Module to check.
|
||||||
|
* @param section Section to check.
|
||||||
|
* @return Wether the module is stealth.
|
||||||
|
*/
|
||||||
|
isModuleStealth(module: CoreCourseModuleData, section?: CoreCourseWSSection): boolean {
|
||||||
|
// visibleoncoursepage can be 1 for teachers when the section is hidden.
|
||||||
|
return !!module.visible && (!module.visibleoncoursepage || (!!section && !section.visible));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module is visible by the user.
|
||||||
|
*
|
||||||
|
* @param module Module to check.
|
||||||
|
* @param section Section to check. Omitted if not defined.
|
||||||
|
* @return Wether the section is visible by the user.
|
||||||
|
*/
|
||||||
|
canUserViewModule(module: CoreCourseModuleData, section?: CoreCourseWSSection): boolean {
|
||||||
|
return module.uservisible !== false && (!section || CoreCourseHelper.canUserViewSection(section));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Section is stealth.
|
||||||
|
* This should not be true on Moodle 4.0 onwards.
|
||||||
|
*
|
||||||
|
* @param section Section to check.
|
||||||
|
* @return Wether section is stealth (accessible but not visible to students).
|
||||||
|
*/
|
||||||
|
isSectionStealth(section: CoreCourseWSSection): boolean {
|
||||||
|
return section.hiddenbynumsections === 1 || section.id === CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Section is visible by the user.
|
||||||
|
*
|
||||||
|
* @param section Section to check.
|
||||||
|
* @return Wether the section is visible by the user.
|
||||||
|
*/
|
||||||
|
canUserViewSection(section: CoreCourseWSSection): boolean {
|
||||||
|
return section.uservisible !== false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate completion data of a module.
|
* Calculate completion data of a module.
|
||||||
*
|
*
|
||||||
|
|
|
@ -33,7 +33,7 @@ import { CoreError } from '@classes/errors/error';
|
||||||
import { CoreWSFile, CoreWSExternalWarning } from '@services/ws';
|
import { CoreWSFile, CoreWSExternalWarning } from '@services/ws';
|
||||||
import { CHECK_UPDATES_TIMES_TABLE, CoreCourseCheckUpdatesDBRecord } from './database/module-prefetch';
|
import { CHECK_UPDATES_TIMES_TABLE, CoreCourseCheckUpdatesDBRecord } from './database/module-prefetch';
|
||||||
import { CoreFileSizeSum } from '@services/plugin-file-delegate';
|
import { CoreFileSizeSum } from '@services/plugin-file-delegate';
|
||||||
import { CoreCourseModuleData } from './course-helper';
|
import { CoreCourseHelper, CoreCourseModuleData } from './course-helper';
|
||||||
|
|
||||||
const ROOT_CACHE_KEY = 'mmCourse:';
|
const ROOT_CACHE_KEY = 'mmCourse:';
|
||||||
|
|
||||||
|
@ -956,7 +956,7 @@ export class CoreCourseModulePrefetchDelegateService extends CoreDelegate<CoreCo
|
||||||
* @return Promise resolved with true if downloadable, false otherwise.
|
* @return Promise resolved with true if downloadable, false otherwise.
|
||||||
*/
|
*/
|
||||||
async isModuleDownloadable(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> {
|
async isModuleDownloadable(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> {
|
||||||
if ('uservisible' in module && module.uservisible === false) {
|
if ('uservisible' in module && !CoreCourseHelper.canUserViewModule(module)) {
|
||||||
// Module isn't visible by the user, cannot be downloaded.
|
// Module isn't visible by the user, cannot be downloaded.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import {
|
||||||
ViewChild,
|
ViewChild,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
OnInit,
|
OnInit,
|
||||||
AfterContentInit,
|
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
Optional,
|
Optional,
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
|
@ -56,7 +55,7 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise';
|
||||||
templateUrl: 'core-editor-rich-text-editor.html',
|
templateUrl: 'core-editor-rich-text-editor.html',
|
||||||
styleUrls: ['rich-text-editor.scss'],
|
styleUrls: ['rich-text-editor.scss'],
|
||||||
})
|
})
|
||||||
export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy {
|
export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
// Based on: https://github.com/judgewest2000/Ionic3RichText/
|
// Based on: https://github.com/judgewest2000/Ionic3RichText/
|
||||||
// @todo: Anchor button, fullscreen...
|
// @todo: Anchor button, fullscreen...
|
||||||
|
@ -149,7 +148,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.canScanQR = CoreUtils.canScanQR();
|
this.canScanQR = CoreUtils.canScanQR();
|
||||||
|
@ -159,9 +158,9 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init editor.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngAfterContentInit(): Promise<void> {
|
async ngAfterViewInit(): Promise<void> {
|
||||||
this.rteEnabled = await CoreDomUtils.isRichTextEditorEnabled();
|
this.rteEnabled = await CoreDomUtils.isRichTextEditorEnabled();
|
||||||
|
|
||||||
await this.waitLoadingsDone();
|
await this.waitLoadingsDone();
|
||||||
|
@ -202,13 +201,14 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
|
||||||
|
|
||||||
this.deleteDraftOnSubmitOrCancel();
|
this.deleteDraftOnSubmitOrCancel();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const ionItem = this.element.closest<HTMLIonItemElement>('ion-item');
|
||||||
* @inheritdoc
|
if (!ionItem) {
|
||||||
*/
|
return;
|
||||||
async ngAfterViewInit(): Promise<void> {
|
}
|
||||||
const label = this.element.closest('ion-item')?.querySelector('ion-label');
|
ionItem.classList.add('item-rte');
|
||||||
|
|
||||||
|
const label = ionItem.querySelector('ion-label');
|
||||||
|
|
||||||
if (!label) {
|
if (!label) {
|
||||||
return;
|
return;
|
||||||
|
@ -773,7 +773,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
|
||||||
* @param event Event.
|
* @param event Event.
|
||||||
*/
|
*/
|
||||||
stopBubble(event: Event): void {
|
stopBubble(event: Event): void {
|
||||||
if (event.type != 'touchend' &&event.type != 'mouseup' && event.type != 'keyup') {
|
if (event.type != 'touchend' && event.type != 'mouseup' && event.type != 'keyup') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { makeSingleton } from '@singletons';
|
||||||
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
|
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
|
||||||
import { CoreLogger } from '@singletons/logger';
|
import { CoreLogger } from '@singletons/logger';
|
||||||
import { CoreSite } from '@classes/site';
|
import { CoreSite } from '@classes/site';
|
||||||
|
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper service to provide filter functionalities.
|
* Helper service to provide filter functionalities.
|
||||||
|
@ -159,7 +160,7 @@ export class CoreFilterHelperProvider {
|
||||||
sections.forEach((section) => {
|
sections.forEach((section) => {
|
||||||
if (section.modules) {
|
if (section.modules) {
|
||||||
section.modules.forEach((module) => {
|
section.modules.forEach((module) => {
|
||||||
if (module.uservisible) {
|
if (CoreCourseHelper.canUserViewModule(module, section)) {
|
||||||
contexts.push({
|
contexts.push({
|
||||||
contextlevel: 'module',
|
contextlevel: 'module',
|
||||||
instanceid: module.id,
|
instanceid: module.id,
|
||||||
|
|
|
@ -13,8 +13,8 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { AfterViewInit, Component, ElementRef, OnDestroy, Optional } from '@angular/core';
|
import { AfterViewInit, Component, ElementRef, OnDestroy } from '@angular/core';
|
||||||
import { IonContent, IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreGrades } from '@features/grades/services/grades';
|
import { CoreGrades } from '@features/grades/services/grades';
|
||||||
|
@ -59,7 +59,6 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
|
||||||
constructor(
|
constructor(
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected element: ElementRef<HTMLElement>,
|
protected element: ElementRef<HTMLElement>,
|
||||||
@Optional() protected content?: IonContent,
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId', { route });
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId', { route });
|
||||||
|
@ -170,11 +169,9 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
if (row) {
|
if (row) {
|
||||||
this.toggleRow(row, true);
|
this.toggleRow(row, true);
|
||||||
await CoreUtils.nextTick();
|
|
||||||
|
|
||||||
CoreDomUtils.scrollToElementBySelector(
|
CoreDomUtils.scrollViewToElement(
|
||||||
this.element.nativeElement,
|
this.element.nativeElement,
|
||||||
this.content,
|
|
||||||
'#grade-' + row.id,
|
'#grade-' + row.id,
|
||||||
);
|
);
|
||||||
this.gradeId = undefined;
|
this.gradeId = undefined;
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
import { Component, ViewChild, ElementRef, OnInit, ChangeDetectorRef } from '@angular/core';
|
import { Component, ViewChild, ElementRef, OnInit, ChangeDetectorRef } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
||||||
import { IonContent, IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
@ -46,7 +46,6 @@ import { CoreText } from '@singletons/text';
|
||||||
})
|
})
|
||||||
export class CoreLoginEmailSignupPage implements OnInit {
|
export class CoreLoginEmailSignupPage implements OnInit {
|
||||||
|
|
||||||
@ViewChild(IonContent) content?: IonContent;
|
|
||||||
@ViewChild(CoreRecaptchaComponent) recaptchaComponent?: CoreRecaptchaComponent;
|
@ViewChild(CoreRecaptchaComponent) recaptchaComponent?: CoreRecaptchaComponent;
|
||||||
@ViewChild('ageForm') ageFormElement?: ElementRef;
|
@ViewChild('ageForm') ageFormElement?: ElementRef;
|
||||||
@ViewChild('signupFormEl') signupFormElement?: ElementRef;
|
@ViewChild('signupFormEl') signupFormElement?: ElementRef;
|
||||||
|
@ -285,9 +284,8 @@ export class CoreLoginEmailSignupPage implements OnInit {
|
||||||
this.changeDetector.detectChanges();
|
this.changeDetector.detectChanges();
|
||||||
|
|
||||||
// Scroll to the first element with errors.
|
// Scroll to the first element with errors.
|
||||||
const errorFound = CoreDomUtils.scrollToInputError(
|
const errorFound = await CoreDomUtils.scrollViewToInputError(
|
||||||
this.elementRef.nativeElement,
|
this.elementRef.nativeElement,
|
||||||
this.content,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!errorFound) {
|
if (!errorFound) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { Type } from '@angular/core';
|
||||||
|
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
import { CoreCourse } from '@features/course/services/course';
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { CoreCourseModuleData } from '@features/course/services/course-helper';
|
import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
|
||||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
||||||
import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index';
|
import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index';
|
||||||
import {
|
import {
|
||||||
|
@ -105,7 +105,7 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forCoursePage && this.handlerSchema.coursepagemethod && module.visibleoncoursepage !== 0) {
|
if (forCoursePage && this.handlerSchema.coursepagemethod && !CoreCourseHelper.isModuleStealth(module)) {
|
||||||
// Call the method to get the course page template.
|
// Call the method to get the course page template.
|
||||||
const method = this.handlerSchema.coursepagemethod;
|
const method = this.handlerSchema.coursepagemethod;
|
||||||
this.loadCoursePageTemplate(module, courseId, handlerData, method);
|
this.loadCoursePageTemplate(module, courseId, handlerData, method);
|
||||||
|
|
|
@ -220,6 +220,53 @@ export class CoreDomUtilsProvider {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a function when an element has been slotted.
|
||||||
|
*
|
||||||
|
* @param element HTML Element inside an ion-content to wait for slot.
|
||||||
|
* @param callback Function to execute on resize.
|
||||||
|
*/
|
||||||
|
onElementSlot(element: HTMLElement, callback: (ev?: Event) => void): void {
|
||||||
|
if (!element.slot) {
|
||||||
|
// Element not declared to be slotted.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slotName = element.slot;
|
||||||
|
if (element.assignedSlot?.name === slotName) {
|
||||||
|
// Slot already assigned.
|
||||||
|
callback();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = element.closest('ion-content');
|
||||||
|
if (!content || !content.shadowRoot) {
|
||||||
|
// Cannot find content.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slots = content.shadowRoot.querySelectorAll('slot');
|
||||||
|
const slot = Array.from(slots).find((slot) => slot.name === slotName);
|
||||||
|
|
||||||
|
if (!slot) {
|
||||||
|
// Slot not found.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slotListener = () => {
|
||||||
|
if (element.assignedSlot?.name !== slotName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
|
// It would happen only once.
|
||||||
|
slot.removeEventListener('slotchange', slotListener);
|
||||||
|
};
|
||||||
|
|
||||||
|
slot.addEventListener('slotchange', slotListener);;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Window resize is widely checked and may have many performance issues, debouce usage is needed to avoid calling it too much.
|
* Window resize is widely checked and may have many performance issues, debouce usage is needed to avoid calling it too much.
|
||||||
* This function helps setting up the debounce feature and remove listener easily.
|
* This function helps setting up the debounce feature and remove listener easily.
|
||||||
|
@ -229,7 +276,9 @@ export class CoreDomUtilsProvider {
|
||||||
* @return Event observer to call off when finished.
|
* @return Event observer to call off when finished.
|
||||||
*/
|
*/
|
||||||
onWindowResize(resizeFunction: (ev?: Event) => void, debounceDelay = 20): CoreEventObserver {
|
onWindowResize(resizeFunction: (ev?: Event) => void, debounceDelay = 20): CoreEventObserver {
|
||||||
const resizeListener = CoreUtils.debounce((ev?: Event) => {
|
const resizeListener = CoreUtils.debounce(async (ev?: Event) => {
|
||||||
|
await this.waitForResizeDone();
|
||||||
|
|
||||||
resizeFunction(ev);
|
resizeFunction(ev);
|
||||||
}, debounceDelay);
|
}, debounceDelay);
|
||||||
|
|
||||||
|
@ -461,15 +510,30 @@ export class CoreDomUtilsProvider {
|
||||||
/**
|
/**
|
||||||
* Focus an element and open keyboard.
|
* Focus an element and open keyboard.
|
||||||
*
|
*
|
||||||
* @param el HTML element to focus.
|
* @param focusElement HTML element to focus.
|
||||||
*/
|
*/
|
||||||
focusElement(el: HTMLElement): void {
|
async focusElement(focusElement: HTMLElement): Promise<void> {
|
||||||
if (el?.focus) {
|
let retries = 10;
|
||||||
el.focus();
|
|
||||||
if (CoreApp.isAndroid() && this.supportsInputKeyboard(el)) {
|
if (!focusElement.focus) {
|
||||||
|
throw new CoreError('Element to focus cannot be focused');
|
||||||
|
}
|
||||||
|
|
||||||
|
while (retries > 0 && focusElement !== document.activeElement) {
|
||||||
|
focusElement.focus();
|
||||||
|
|
||||||
|
if (focusElement === document.activeElement) {
|
||||||
|
await CoreUtils.nextTick();
|
||||||
|
if (CoreApp.isAndroid() && this.supportsInputKeyboard(focusElement)) {
|
||||||
// On some Android versions the keyboard doesn't open automatically.
|
// On some Android versions the keyboard doesn't open automatically.
|
||||||
CoreApp.openKeyboard();
|
CoreApp.openKeyboard();
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO Probably a Mutation Observer would get this working.
|
||||||
|
await CoreUtils.wait(50);
|
||||||
|
retries--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -656,50 +720,54 @@ export class CoreDomUtilsProvider {
|
||||||
/**
|
/**
|
||||||
* Retrieve the position of a element relative to another element.
|
* Retrieve the position of a element relative to another element.
|
||||||
*
|
*
|
||||||
* @param container Element to search in.
|
* @param element Element to search in.
|
||||||
* @param selector Selector to find the element to gets the position.
|
* @param selector Selector to find the element to gets the position.
|
||||||
* @param positionParentClass Parent Class where to stop calculating the position. Default inner-scroll.
|
* @param positionParentClass Parent Class where to stop calculating the position. Default inner-scroll.
|
||||||
* @return positionLeft, positionTop of the element relative to.
|
* @return positionLeft, positionTop of the element relative to.
|
||||||
|
* @deprecated since app 4.0. Use getRelativeElementPosition instead.
|
||||||
*/
|
*/
|
||||||
getElementXY(container: HTMLElement, selector: undefined, positionParentClass?: string): number[];
|
getElementXY(element: HTMLElement, selector?: string, positionParentClass = 'inner-scroll'): [number, number] | null {
|
||||||
getElementXY(container: HTMLElement, selector: string, positionParentClass?: string): number[] | null;
|
if (selector) {
|
||||||
getElementXY(container: HTMLElement, selector?: string, positionParentClass?: string): number[] | null {
|
const foundElement = element.querySelector<HTMLElement>(selector);
|
||||||
let element: HTMLElement | null = <HTMLElement> (selector ? container.querySelector(selector) : container);
|
if (!foundElement) {
|
||||||
let positionTop = 0;
|
// Element not found.
|
||||||
let positionLeft = 0;
|
|
||||||
|
|
||||||
if (!positionParentClass) {
|
|
||||||
positionParentClass = 'inner-scroll';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!element) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (element) {
|
element = foundElement;
|
||||||
positionLeft += (element.offsetLeft - element.scrollLeft + element.clientLeft);
|
|
||||||
positionTop += (element.offsetTop - element.scrollTop + element.clientTop);
|
|
||||||
|
|
||||||
const offsetElement = element.offsetParent;
|
|
||||||
element = element.parentElement;
|
|
||||||
|
|
||||||
// Every parent class has to be checked but the position has to be got form offsetParent.
|
|
||||||
while (offsetElement != element && element) {
|
|
||||||
// If positionParentClass element is reached, stop adding tops.
|
|
||||||
if (element.className.indexOf(positionParentClass) != -1) {
|
|
||||||
element = null;
|
|
||||||
} else {
|
|
||||||
element = element.parentElement;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, check again.
|
const parent = element.closest<HTMLElement>(`.${positionParentClass}`);
|
||||||
if (element?.className.indexOf(positionParentClass) != -1) {
|
if (!parent) {
|
||||||
element = null;
|
return null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [positionLeft, positionTop];
|
const position = CoreDomUtils.getRelativeElementPosition(element, parent);
|
||||||
|
|
||||||
|
// Calculate the top and left positions.
|
||||||
|
return [
|
||||||
|
Math.ceil(position.x),
|
||||||
|
Math.ceil(position.y),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the position of a element relative to another element.
|
||||||
|
*
|
||||||
|
* @param element Element to get the position.
|
||||||
|
* @param parent Parent element to get relative position.
|
||||||
|
* @return X and Y position.
|
||||||
|
*/
|
||||||
|
getRelativeElementPosition(element: HTMLElement, parent: HTMLElement): CoreCoordinates {
|
||||||
|
// Get the top, left coordinates of two elements
|
||||||
|
const elementRectangle = element.getBoundingClientRect();
|
||||||
|
const parentRectangle = parent.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Calculate the top and left positions.
|
||||||
|
return {
|
||||||
|
x: elementRectangle.x - parentRectangle.x,
|
||||||
|
y: elementRectangle.y - parentRectangle.y,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1032,13 +1100,11 @@ export class CoreDomUtilsProvider {
|
||||||
* @param selector Selector to search.
|
* @param selector Selector to search.
|
||||||
*/
|
*/
|
||||||
removeElement(element: HTMLElement, selector: string): void {
|
removeElement(element: HTMLElement, selector: string): void {
|
||||||
if (element) {
|
|
||||||
const selected = element.querySelector(selector);
|
const selected = element.querySelector(selector);
|
||||||
if (selected) {
|
if (selected) {
|
||||||
selected.remove();
|
selected.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search and remove a certain element from an HTML code.
|
* Search and remove a certain element from an HTML code.
|
||||||
|
@ -1134,9 +1200,9 @@ export class CoreDomUtilsProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Treat video posters.
|
// Treat video posters.
|
||||||
if (media.tagName == 'VIDEO' && media.getAttribute('poster')) {
|
|
||||||
const currentPoster = media.getAttribute('poster');
|
const currentPoster = media.getAttribute('poster');
|
||||||
const newPoster = paths[CoreTextUtils.decodeURIComponent(currentPoster!)];
|
if (media.tagName == 'VIDEO' && currentPoster) {
|
||||||
|
const newPoster = paths[CoreTextUtils.decodeURIComponent(currentPoster)];
|
||||||
if (newPoster !== undefined) {
|
if (newPoster !== undefined) {
|
||||||
media.setAttribute('poster', newPoster);
|
media.setAttribute('poster', newPoster);
|
||||||
}
|
}
|
||||||
|
@ -1173,8 +1239,8 @@ export class CoreDomUtilsProvider {
|
||||||
* @return Returns a promise which is resolved when the scroll has completed.
|
* @return Returns a promise which is resolved when the scroll has completed.
|
||||||
* @deprecated since 3.9.5. Use directly the IonContent class.
|
* @deprecated since 3.9.5. Use directly the IonContent class.
|
||||||
*/
|
*/
|
||||||
scrollTo(content: IonContent, x: number, y: number, duration?: number): Promise<void> {
|
scrollTo(content: IonContent, x: number, y: number, duration = 0): Promise<void> {
|
||||||
return content.scrollToPoint(x, y, duration || 0);
|
return content.scrollToPoint(x, y, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1197,7 +1263,7 @@ export class CoreDomUtilsProvider {
|
||||||
* @return Returns a promise which is resolved when the scroll has completed.
|
* @return Returns a promise which is resolved when the scroll has completed.
|
||||||
* @deprecated since 3.9.5. Use directly the IonContent class.
|
* @deprecated since 3.9.5. Use directly the IonContent class.
|
||||||
*/
|
*/
|
||||||
scrollToTop(content: IonContent, duration?: number): Promise<void> {
|
scrollToTop(content: IonContent, duration = 0): Promise<void> {
|
||||||
return content.scrollToTop(duration);
|
return content.scrollToTop(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1244,7 +1310,7 @@ export class CoreDomUtilsProvider {
|
||||||
const scrollElement = await content.getScrollElement();
|
const scrollElement = await content.getScrollElement();
|
||||||
|
|
||||||
return scrollElement.scrollTop || 0;
|
return scrollElement.scrollTop || 0;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1252,51 +1318,34 @@ export class CoreDomUtilsProvider {
|
||||||
/**
|
/**
|
||||||
* Scroll to a certain element.
|
* Scroll to a certain element.
|
||||||
*
|
*
|
||||||
* @param content The content that must be scrolled.
|
|
||||||
* @param element The element to scroll to.
|
* @param element The element to scroll to.
|
||||||
* @param scrollParentClass Parent class where to stop calculating the position. Default inner-scroll.
|
* @param selector Selector to find the element to scroll to inside the defined element.
|
||||||
* @param duration Duration of the scroll animation in milliseconds.
|
* @param duration Duration of the scroll animation in milliseconds.
|
||||||
* @return True if the element is found, false otherwise.
|
* @return Wether the scroll suceeded.
|
||||||
*/
|
*/
|
||||||
scrollToElement(content: IonContent, element: HTMLElement, scrollParentClass?: string, duration?: number): boolean {
|
async scrollViewToElement(element: HTMLElement, selector?: string, duration = 0): Promise<boolean> {
|
||||||
const position = this.getElementXY(element, undefined, scrollParentClass);
|
await CoreDomUtils.waitToBeInDOM(element);
|
||||||
if (!position) {
|
|
||||||
|
if (selector) {
|
||||||
|
const foundElement = element.querySelector<HTMLElement>(selector);
|
||||||
|
if (!foundElement) {
|
||||||
|
// Element not found.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
content.scrollToPoint(position[0], position[1], duration || 0);
|
element = foundElement;
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const content = element.closest<HTMLIonContentElement>('ion-content') ?? undefined;
|
||||||
* Scroll to a certain element using a selector to find it.
|
if (!content) {
|
||||||
*
|
// Content to scroll, not found.
|
||||||
* @param container The element that contains the element that must be scrolled.
|
|
||||||
* @param content The content that must be scrolled.
|
|
||||||
* @param selector Selector to find the element to scroll to.
|
|
||||||
* @param scrollParentClass Parent class where to stop calculating the position. Default inner-scroll.
|
|
||||||
* @param duration Duration of the scroll animation in milliseconds.
|
|
||||||
* @return True if the element is found, false otherwise.
|
|
||||||
*/
|
|
||||||
scrollToElementBySelector(
|
|
||||||
container: HTMLElement | null,
|
|
||||||
content: IonContent | undefined,
|
|
||||||
selector: string,
|
|
||||||
scrollParentClass?: string,
|
|
||||||
duration?: number,
|
|
||||||
): boolean {
|
|
||||||
if (!container || !content) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const position = this.getElementXY(container, selector, scrollParentClass);
|
const position = CoreDomUtils.getRelativeElementPosition(element, content);
|
||||||
if (!position) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
content.scrollToPoint(position[0], position[1], duration || 0);
|
await content.scrollToPoint(position.x, position.y, duration);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -1308,12 +1357,71 @@ export class CoreDomUtilsProvider {
|
||||||
* Search for an input with error (core-input-error directive) and scrolls to it if found.
|
* Search for an input with error (core-input-error directive) and scrolls to it if found.
|
||||||
*
|
*
|
||||||
* @param container The element that contains the element that must be scrolled.
|
* @param container The element that contains the element that must be scrolled.
|
||||||
* @param content The content that must be scrolled.
|
|
||||||
* @param scrollParentClass Parent class where to stop calculating the position. Default inner-scroll.
|
|
||||||
* @return True if the element is found, false otherwise.
|
* @return True if the element is found, false otherwise.
|
||||||
*/
|
*/
|
||||||
scrollToInputError(container: HTMLElement | null, content?: IonContent, scrollParentClass?: string): boolean {
|
async scrollViewToInputError(container: HTMLElement): Promise<boolean> {
|
||||||
return this.scrollToElementBySelector(container, content, '.core-input-error', scrollParentClass);
|
return this.scrollViewToElement(container, '.core-input-error');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll to a certain element.
|
||||||
|
*
|
||||||
|
* @param content Not used anymore.
|
||||||
|
* @param element The element to scroll to.
|
||||||
|
* @param scrollParentClass Not used anymore.
|
||||||
|
* @param duration Duration of the scroll animation in milliseconds.
|
||||||
|
* @return True if the element is found, false otherwise.
|
||||||
|
* @deprecated since app 4.0 Use scrollViewToElement instead.
|
||||||
|
*/
|
||||||
|
scrollToElement(content: IonContent, element: HTMLElement, scrollParentClass?: string, duration = 0): boolean {
|
||||||
|
CoreDomUtils.scrollViewToElement(element, undefined, duration);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll to a certain element using a selector to find it.
|
||||||
|
*
|
||||||
|
* @param container The element that contains the element that must be scrolled.
|
||||||
|
* @param content Not used anymore.
|
||||||
|
* @param selector Selector to find the element to scroll to.
|
||||||
|
* @param scrollParentClass Not used anymore.
|
||||||
|
* @param duration Duration of the scroll animation in milliseconds.
|
||||||
|
* @return True if the element is found, false otherwise.
|
||||||
|
* @deprecated since app 4.0 Use scrollViewToElement instead.
|
||||||
|
*/
|
||||||
|
scrollToElementBySelector(
|
||||||
|
container: HTMLElement | null,
|
||||||
|
content: unknown | null,
|
||||||
|
selector: string,
|
||||||
|
scrollParentClass?: string,
|
||||||
|
duration = 0,
|
||||||
|
): boolean {
|
||||||
|
if (!container || !content) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreDomUtils.scrollViewToElement(container, selector, duration);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for an input with error (core-input-error directive) and scrolls to it if found.
|
||||||
|
*
|
||||||
|
* @param container The element that contains the element that must be scrolled.
|
||||||
|
* @return True if the element is found, false otherwise.
|
||||||
|
* @deprecated since app 4.0 Use scrollViewToInputError instead.
|
||||||
|
*/
|
||||||
|
scrollToInputError(container: HTMLElement | null): boolean {
|
||||||
|
if (!container) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollViewToInputError(container);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2106,6 +2214,7 @@ export class CoreDomUtilsProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In iOS the resize event is triggered before the window size changes. Wait for the size to change.
|
* In iOS the resize event is triggered before the window size changes. Wait for the size to change.
|
||||||
|
* Use of this function is discouraged. Please use onWindowResize to check window resize event.
|
||||||
*
|
*
|
||||||
* @param windowWidth Initial window width.
|
* @param windowWidth Initial window width.
|
||||||
* @param windowHeight Initial window height.
|
* @param windowHeight Initial window height.
|
||||||
|
@ -2318,3 +2427,11 @@ export enum VerticalPoint {
|
||||||
MID = 'mid',
|
MID = 'mid',
|
||||||
BOTTOM = 'bottom',
|
BOTTOM = 'bottom',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coordinates of an element.
|
||||||
|
*/
|
||||||
|
export type CoreCoordinates = {
|
||||||
|
x: number; // X axis coordinates.
|
||||||
|
y: number; // Y axis coordinates.
|
||||||
|
};
|
||||||
|
|
|
@ -60,7 +60,7 @@ export class CoreForms {
|
||||||
/**
|
/**
|
||||||
* Trigger form cancelled event.
|
* Trigger form cancelled event.
|
||||||
*
|
*
|
||||||
* @param form Form element.
|
* @param formRef Form element.
|
||||||
* @param siteId The site affected. If not provided, no site affected.
|
* @param siteId The site affected. If not provided, no site affected.
|
||||||
*/
|
*/
|
||||||
static triggerFormCancelledEvent(formRef?: ElementRef | HTMLFormElement | undefined, siteId?: string): void {
|
static triggerFormCancelledEvent(formRef?: ElementRef | HTMLFormElement | undefined, siteId?: string): void {
|
||||||
|
@ -77,7 +77,7 @@ export class CoreForms {
|
||||||
/**
|
/**
|
||||||
* Trigger form submitted event.
|
* Trigger form submitted event.
|
||||||
*
|
*
|
||||||
* @param form Form element.
|
* @param formRef Form element.
|
||||||
* @param online Whether the action was done in offline or not.
|
* @param online Whether the action was done in offline or not.
|
||||||
* @param siteId The site affected. If not provided, no site affected.
|
* @param siteId The site affected. If not provided, no site affected.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: '';
|
content: '';
|
||||||
height: 60px;
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@include position(null, 0, 0, 0);
|
@include position(null, 0, 0, 0);
|
||||||
background: linear-gradient(to bottom, rgba(var(--background-gradient-rgb), 0) calc(100% - var(--gradient-size)), rgba(var(--background-gradient-rgb), 1) calc(100% - 4px));
|
background: linear-gradient(to bottom, rgba(var(--background-gradient-rgb), 0) calc(100% - var(--gradient-size)), rgba(var(--background-gradient-rgb), 1) calc(100% - 4px));
|
||||||
|
|
|
@ -985,6 +985,18 @@ ion-chip {
|
||||||
color: var(--ion-color-base);
|
color: var(--ion-color-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.ion-color {
|
||||||
|
&.ion-color-light,
|
||||||
|
&.ion-color-medium,
|
||||||
|
&.ion-color-dark,
|
||||||
|
&.ion-color-secondary {
|
||||||
|
color: var(--gray-900);
|
||||||
|
&.chip-outline {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-searchbar {
|
ion-searchbar {
|
||||||
|
@ -1243,7 +1255,7 @@ ion-datetime.datetime-disabled {
|
||||||
|
|
||||||
// Make links clickable when inside radio or checkbox items. Style part.
|
// Make links clickable when inside radio or checkbox items. Style part.
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
ion-item.item-multiple-inputs:hover::part(native) {
|
ion-item.item-multiple-inputs:not(.item-rte):hover::part(native) {
|
||||||
color: var(--color-hover);
|
color: var(--color-hover);
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
@ -1264,7 +1276,7 @@ ion-datetime.datetime-disabled {
|
||||||
|
|
||||||
// It fixes the click on links where ion-ripple-effect is present.
|
// It fixes the click on links where ion-ripple-effect is present.
|
||||||
// Make links clickable when inside radio or checkbox items. Pointer and cursor part.
|
// Make links clickable when inside radio or checkbox items. Pointer and cursor part.
|
||||||
ion-item.item-multiple-inputs:not(.only-links),
|
ion-item.item-multiple-inputs:not(.only-links):not(.item-rte),
|
||||||
ion-item.ion-activatable:not(.only-links) {
|
ion-item.ion-activatable:not(.only-links) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
ion-label {
|
ion-label {
|
||||||
|
|
Loading…
Reference in New Issue