Merge pull request #3193 from crazyserver/MOBILE-3814

Mobile 3814
main
Dani Palou 2022-03-22 07:48:10 +01:00 committed by GitHub
commit be28e1c2e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 620 additions and 521 deletions

View File

@ -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;

View File

@ -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>
<ion-item>
<ion-label>
<ion-row>
<ion-col *ngIf="hasOffline && eventId && eventId < 0">
<ion-button expand="block" fill="outline" (click)="discard()">
{{ 'core.discard' | translate }}
</ion-button>
</ion-col>
<ion-col>
<ion-button expand="block" (click)="submit()" [disabled]="!form.valid" type="submit">
{{ 'core.save' | translate }}
</ion-button>
</ion-col>
</ion-row>
</ion-label>
</ion-item>
</form> </form>
<div collapsible-footer appearOnBottom *ngIf="loaded && !error" slot="fixed">
<div class="list-item-limited-width adaptable-buttons-row">
<ion-button *ngIf="hasOffline && eventId && eventId < 0" expand="block" fill="outline" (click)="discard()"
class="ion-margin ion-text-wrap">
{{ 'core.discard' | translate }}
</ion-button>
<ion-button expand="block" (click)="submit()" [disabled]="!form.valid" type="submit" class="ion-margin ion-text-wrap">
{{ 'core.save' | translate }}
</ion-button>
</div>
</div>
</core-loading> </core-loading>
</ion-content> </ion-content>

View File

@ -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.

View File

@ -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]);
} }
} }

View File

@ -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();
} }
});
} }
/** /**

View File

@ -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();
} }
} }

View File

@ -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,
); );
} }

View File

@ -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, '#addon-mod_forum-post-' + scrollTo,
this.content, );
'#addon-mod_forum-post-' + scrollTo,
);
});
} }
} }

View File

@ -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) {

View File

@ -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,
); );
} }

View File

@ -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}`,
); );
} }

View File

@ -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();
}
} }
/** /**

View File

@ -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();
}
} }
/** /**

View File

@ -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';

View File

@ -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();
}
} }
/** /**

View File

@ -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 = parentPage?.querySelector<HTMLIonHeaderElement>(':scope > ion-header');
const header = this.searchHeaderInPage(parentPage); if (header && getComputedStyle(header).display !== 'none') {
if (header && getComputedStyle(header, null).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.

View File

@ -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>

View File

@ -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,58 +56,51 @@ 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'; }
// By default, don't autocapitalize and autocorrect. this.setData(this.input);
if (!this.input.getAttribute('autocorrect')) {
this.input.setAttribute('autocorrect', 'off'); // By default, don't autocapitalize and autocorrect.
} if (!this.input.getAttribute('autocorrect')) {
if (!this.input.getAttribute('autocapitalize')) { this.input.setAttribute('autocorrect', 'off');
this.input.setAttribute('autocapitalize', 'none'); }
} if (!this.input.getAttribute('autocapitalize')) {
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 (!this.input) {
return;
}
this.setData(this.input);
// In Android, the keyboard is closed when the input type changes. Focus it again.
if (isFocused && CoreApp.isAndroid()) { if (isFocused && CoreApp.isAndroid()) {
// In Android, the keyboard is closed when the input type changes. Focus it again. CoreDomUtils.focusElement(this.input);
setTimeout(() => {
CoreDomUtils.focusElement(this.input!);
}, 400);
} }
} }
/**
* 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';
}
} }

View File

@ -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;
* Function to focus the element.
* if ('getInputElement' in focusElement) {
* @param retries Internal param to stop retrying on 0. // If it's an Ionic element get the right input to use.
*/ focusElement.componentOnReady && await focusElement.componentOnReady();
protected setFocus(retries = 10): void { focusElement = await focusElement.getInputElement();
if (retries == 0) { }
if (!focusElement) {
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);
} }
} }

View File

@ -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(() => {

View File

@ -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);

View File

@ -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.domPromise = CoreDomUtils.waitToBeInDOM(this.element);
await this.domPromise;
this.content = this.element.closest('ion-content'); this.content = this.element.closest('ion-content');
if (!this.content) { if (!this.content) {
if(retries > 0) { return;
await CoreUtils.nextTicks(50); }
this.ngOnInit(retries - 1); // 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();
} }
} }

View File

@ -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(
`#${href}, [name='${href}']`, container,
); `#${href}, [name='${href}']`,
);
}
return; return;
} }

View File

@ -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;
} }
} }

View File

@ -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, '.item.item-current',
this.content, );
'.item.item-current',
);
}, 300);
} }
/** /**

View File

@ -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 }}

View File

@ -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,

View File

@ -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>

View File

@ -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);
} }
} }

View File

@ -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;
} }

View File

@ -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>

View File

@ -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.
* *

View File

@ -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;
} }

View File

@ -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();

View File

@ -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,

View File

@ -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;

View File

@ -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) {

View File

@ -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);

View File

@ -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) {
// On some Android versions the keyboard doesn't open automatically. throw new CoreError('Element to focus cannot be focused');
CoreApp.openKeyboard(); }
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.
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; return null;
}
if (!positionParentClass) { element = foundElement;
positionParentClass = 'inner-scroll';
} }
if (!element) { const parent = element.closest<HTMLElement>(`.${positionParentClass}`);
if (!parent) {
return null; return null;
} }
while (element) { const position = CoreDomUtils.getRelativeElementPosition(element, parent);
positionLeft += (element.offsetLeft - element.scrollLeft + element.clientLeft);
positionTop += (element.offsetTop - element.scrollTop + element.clientTop);
const offsetElement = element.offsetParent; // Calculate the top and left positions.
element = element.parentElement; return [
Math.ceil(position.x),
Math.ceil(position.y),
];
}
// Every parent class has to be checked but the position has to be got form offsetParent. /**
while (offsetElement != element && element) { * Retrieve the position of a element relative to another element.
// If positionParentClass element is reached, stop adding tops. *
if (element.className.indexOf(positionParentClass) != -1) { * @param element Element to get the position.
element = null; * @param parent Parent element to get relative position.
} else { * @return X and Y position.
element = element.parentElement; */
} getRelativeElementPosition(element: HTMLElement, parent: HTMLElement): CoreCoordinates {
} // Get the top, left coordinates of two elements
const elementRectangle = element.getBoundingClientRect();
const parentRectangle = parent.getBoundingClientRect();
// Finally, check again. // Calculate the top and left positions.
if (element?.className.indexOf(positionParentClass) != -1) { return {
element = null; x: elementRectangle.x - parentRectangle.x,
} y: elementRectangle.y - parentRectangle.y,
} };
return [positionLeft, positionTop];
} }
/** /**
@ -1032,11 +1100,9 @@ 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();
}
} }
} }
@ -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'); if (media.tagName == 'VIDEO' && currentPoster) {
const newPoster = paths[CoreTextUtils.decodeURIComponent(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) {
return false; if (selector) {
const foundElement = element.querySelector<HTMLElement>(selector);
if (!foundElement) {
// Element not found.
return false;
}
element = foundElement;
} }
content.scrollToPoint(position[0], position[1], duration || 0); const content = element.closest<HTMLIonContentElement>('ion-content') ?? undefined;
if (!content) {
return true; // Content to scroll, not found.
}
/**
* 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 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.
};

View File

@ -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.
*/ */

View File

@ -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));

View File

@ -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 {