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 { CoreUtils } from '@services/utils/utils';
import { CoreNavigator } from '@services/navigator';
import { CoreCourseHelper } from '@features/course/services/course-helper';
/**
* Component to render an "activity modules" block.
@ -67,7 +68,7 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i
}
section.modules.forEach((mod) => {
if (mod.uservisible === false || !CoreCourse.moduleHasView(mod) ||
if (!CoreCourseHelper.canUserViewModule(mod, section) || !CoreCourse.moduleHasView(mod) ||
modFullNames[mod.modname] !== undefined) {
// Ignore this module.
return;

View File

@ -183,7 +183,7 @@
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<p>{{ 'addon.calendar.repeatweeksl' | translate }}</p>
<p class="item-heading">{{ 'addon.calendar.repeatweeksl' | translate }}</p>
</ion-label>
<ion-input type="number" name="repeats" formControlName="repeats" [disabled]="!form.controls.repeat.value">
</ion-input>
@ -231,23 +231,17 @@
<ion-input type="text" name="location" [placeholder]="'core.location' | translate" formControlName="location">
</ion-input>
</ion-item>
<ion-item>
<ion-label>
<ion-row>
<ion-col *ngIf="hasOffline && eventId && eventId < 0">
<ion-button expand="block" fill="outline" (click)="discard()">
</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-col>
<ion-col>
<ion-button expand="block" (click)="submit()" [disabled]="!form.valid" type="submit">
<ion-button expand="block" (click)="submit()" [disabled]="!form.valid" type="submit" class="ion-margin ion-text-wrap">
{{ 'core.save' | translate }}
</ion-button>
</ion-col>
</ion-row>
</ion-label>
</ion-item>
</form>
</div>
</div>
</core-loading>
</ion-content>

View File

@ -15,7 +15,6 @@
import { Injectable } from '@angular/core';
import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter';
import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUrlUtils } from '@services/utils/url';
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.
/**
* Filter some text.
*
* @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).
* @inheritdoc
*/
filter(
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> {
this.template.innerHTML = text;
@ -60,7 +50,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
/**
* Treat video filters. Currently only treating youtube video using video JS.
*
* @param el Video element.
* @param video Video element.
*/
protected treatVideoFilters(video: HTMLElement): void {
// 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.
*/
scrollToFirstUnreadMessage(): void {
if (this.newMessages > 0 && this.content) {
const messages = Array.from(this.hostElement.querySelectorAll('.addon-message-not-mine'));
if (this.newMessages > 0) {
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;
// New messages or beeps, scroll to bottom.
setTimeout(() => this.scrollToBottom());
this.scrollToBottom();
}
protected async loadMessageBeepWho(message: AddonModChatFormattedMessage): Promise<void> {
@ -341,13 +341,12 @@ export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave {
/**
* Scroll bottom when render has finished.
*/
scrollToBottom(): void {
async scrollToBottom(): Promise<void> {
// Need a timeout to leave time to the view to be rendered.
setTimeout(() => {
await CoreUtils.nextTick();
if (!this.viewDestroyed) {
this.content?.scrollToBottom();
}
});
}
/**

View File

@ -352,9 +352,7 @@ export class AddonModDataEditPage implements OnInit {
}
this.jsData!.errors = this.errors;
setTimeout(() => {
this.scrollToFirstError();
});
}
} finally {
modal.dismiss();
@ -449,8 +447,9 @@ export class AddonModDataEditPage implements OnInit {
/**
* Scroll to first error or to the top if not found.
*/
protected scrollToFirstError(): void {
if (!CoreDomUtils.scrollToElementBySelector(this.formElement.nativeElement, this.content, '.addon-data-error')) {
protected async scrollToFirstError(): Promise<void> {
const scrolled = await CoreDomUtils.scrollViewToElement(this.formElement.nativeElement, '.addon-data-error');
if (!scrolled) {
this.content?.scrollToTop();
}
}

View File

@ -20,7 +20,6 @@ import {
OnChanges,
OnDestroy,
OnInit,
Optional,
Output,
SimpleChange,
ViewChild,
@ -41,7 +40,6 @@ import {
import { CoreTag } from '@features/tag/services/tag';
import { Translate } from '@singletons';
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
import { IonContent } from '@ionic/angular';
import { AddonModForumSync } from '../../services/forum-sync';
import { CoreSync } from '@services/sync';
import { CoreTextUtils } from '@services/utils/text';
@ -94,7 +92,6 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
constructor(
protected elementRef: ElementRef,
@Optional() protected content?: IonContent,
) {}
get showForm(): boolean {
@ -308,8 +305,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
this.post.id > 0 ? this.post.id : undefined,
);
this.scrollToForm(5);
} catch (error) {
this.scrollToForm();
} catch {
// Cancelled.
}
}
@ -540,19 +537,11 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
/**
* Scroll to reply/edit form.
*
* @param ticksToWait Number of ticks to wait before scrolling.
* @return Promise resolved when done.
*/
protected async scrollToForm(ticksToWait = 1): Promise<void> {
if (!this.content) {
return;
}
await CoreUtils.nextTicks(ticksToWait);
CoreDomUtils.scrollToElementBySelector(
protected async scrollToForm(): Promise<void> {
await CoreDomUtils.scrollViewToElement(
this.elementRef.nativeElement,
this.content,
'#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;
if (scrollTo) {
// Scroll to the post.
setTimeout(() => {
CoreDomUtils.scrollToElementBySelector(
CoreDomUtils.scrollViewToElement(
this.elementRef.nativeElement,
this.content,
'#addon-mod_forum-post-' + scrollTo,
);
});
}
}

View File

@ -14,7 +14,6 @@
import { Component, OnInit, ViewChild, ElementRef, Input, Type } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { IonContent } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
@ -32,7 +31,6 @@ import { AddonModQuizAttemptWSData, AddonModQuizQuizWSData } from '../../service
})
export class AddonModQuizPreflightModalComponent implements OnInit {
@ViewChild(IonContent) content?: IonContent;
@ViewChild('preflightFormEl') formElement?: ElementRef;
@Input() title!: string;
@ -111,15 +109,14 @@ export class AddonModQuizPreflightModalComponent implements OnInit {
*
* @param e Event.
*/
sendData(e: Event): void {
async sendData(e: Event): Promise<void> {
e.preventDefault();
e.stopPropagation();
if (!this.preflightForm.valid) {
// Form not valid. Scroll to the first element with errors.
const hasScrolled = CoreDomUtils.scrollToInputError(
const hasScrolled = await CoreDomUtils.scrollViewToInputError(
this.elementRef.nativeElement,
this.content,
);
if (!hasScrolled) {

View File

@ -318,10 +318,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
this.loaded = true;
if (slot !== undefined) {
// Scroll to the question. Give some time to the questions to render.
setTimeout(() => {
// Scroll to the question.
this.scrollToQuestion(slot);
}, 2000);
}
}
}
@ -689,9 +687,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
* @param slot Slot of the question to scroll to.
*/
protected scrollToQuestion(slot: number): void {
CoreDomUtils.scrollToElementBySelector(
CoreDomUtils.scrollViewToElement(
this.elementRef.nativeElement,
this.content,
'#addon-mod_quiz-question-' + slot,
);
}

View File

@ -133,10 +133,8 @@ export class AddonModQuizReviewPage implements OnInit {
this.loaded = true;
if (slot !== undefined) {
// Scroll to the question. Give some time to the questions to render.
setTimeout(() => {
// Scroll to the question.
this.scrollToQuestion(slot);
}, 2000);
}
}
}
@ -249,9 +247,8 @@ export class AddonModQuizReviewPage implements OnInit {
* @param slot Slot of the question to scroll to.
*/
protected scrollToQuestion(slot: number): void {
CoreDomUtils.scrollToElementBySelector(
CoreDomUtils.scrollViewToElement(
this.elementRef.nativeElement,
this.content,
`#addon-mod_quiz-question-${slot}`,
);
}

View File

@ -78,13 +78,18 @@ export class AddonQtypeDdImageOrTextQuestion {
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.
bgImgXY[0] *= 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.windowResized();
this.repositionDragsForQuestion();
});
}
@ -409,10 +414,15 @@ export class AddonQtypeDdImageOrTextQuestion {
}
// 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');
drag.style.left = position[0] - 1 + 'px';
drag.style.top = position[1] - 1 + 'px';
drag.style.left = position.x + 'px';
drag.style.top = position.y + 'px';
drag.classList.add('placed');
if (choice) {
@ -458,13 +468,14 @@ export class AddonQtypeDdImageOrTextQuestion {
// Move the element to its original position.
const dragItemHome = this.doc.dragItemHome(Number(drag.getAttribute('dragitemno')));
if (!dragItemHome) {
const ddArea = this.container.querySelector<HTMLElement>('.ddarea');
if (!dragItemHome || !ddArea) {
return;
}
const position = CoreDomUtils.getElementXY(dragItemHome, undefined, 'ddarea');
drag.style.left = position[0] + 'px';
drag.style.top = position[1] + 'px';
const position = CoreDomUtils.getRelativeElementPosition(dragItemHome, ddArea);
drag.style.left = position.x + 'px';
drag.style.top = position.y + 'px';
drag.classList.remove('placed');
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
// limitations under the License.
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCoordinates, CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreEventObserver } from '@singletons/events';
import { CoreLogger } from '@singletons/logger';
import { AddonQtypeDdMarkerQuestionData } from '../component/ddmarker';
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.
*/
@ -36,8 +28,7 @@ export class AddonQtypeDdMarkerQuestion {
protected logger: CoreLogger;
protected afterImageLoadDone = false;
protected drops;
protected topNode;
protected topNode?: HTMLElement | null;
protected nextColourIndex = 0;
protected proportion = 1;
protected selected?: HTMLElement; // Selected element (being "dragged").
@ -123,7 +114,7 @@ export class AddonQtypeDdMarkerQuestion {
return [];
}
const position = CoreDomUtils.getElementXY(bgImg, undefined, 'ddarea');
const position = this.getElementCoordinates(bgImg);
let coordsNumbers = this.parsePoint(bgImgXY);
coordsNumbers = this.makePointProportional(coordsNumbers);
@ -131,13 +122,30 @@ export class AddonQtypeDdMarkerQuestion {
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.
*
* @param coords Coordinates to check.
* @return Whether they're inside the background image.
*/
coordsInImg(coords: AddonQtypeDdMarkerQuestionPoint): boolean {
coordsInImg(coords: CoreCoordinates): boolean {
const bgImg = this.doc.bgImg();
if (!bgImg) {
return false;
@ -177,13 +185,13 @@ export class AddonQtypeDdMarkerQuestion {
const dragging = this.selected;
if (dragging && !drag.classList.contains('unplaced')) {
const position = CoreDomUtils.getElementXY(drag, undefined, 'ddarea');
const position = this.getElementCoordinates(drag);
const bgImg = this.doc.bgImg();
if (!bgImg) {
return;
}
const bgImgPos = CoreDomUtils.getElementXY(bgImg, undefined, 'ddarea');
const bgImgPos = this.getElementCoordinates(bgImg);
position[0] = position[0] - bgImgPos[0] + e.offsetX;
position[1] = position[1] - bgImgPos[1] + e.offsetY;
@ -217,7 +225,7 @@ export class AddonQtypeDdMarkerQuestion {
return [];
}
const position = CoreDomUtils.getElementXY(dragItemHome, undefined, 'ddarea');
const position = this.getElementCoordinates(dragItemHome);
return [position[0], position[1]];
}
@ -317,7 +325,7 @@ export class AddonQtypeDdMarkerQuestion {
* @param colour Colour 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+)?$/)) {
return null;
}
@ -356,7 +364,7 @@ export class AddonQtypeDdMarkerQuestion {
* @param colour Colour 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+)?$/)) {
return null;
}
@ -399,7 +407,7 @@ export class AddonQtypeDdMarkerQuestion {
* @param colour Colour 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+)?)*$/)) {
return null;
}
@ -449,7 +457,7 @@ export class AddonQtypeDdMarkerQuestion {
* @param coordinates "x,y".
* @return Coordinates to the point.
*/
parsePoint(coordinates: string): AddonQtypeDdMarkerQuestionPoint {
parsePoint(coordinates: string): CoreCoordinates {
const bits = coordinates.split(',');
if (bits.length !== 2) {
throw coordinates + ' is not a valid point';
@ -464,7 +472,7 @@ export class AddonQtypeDdMarkerQuestion {
* @param point Point coordinates.
* @return Converted point.
*/
makePointProportional(point: AddonQtypeDdMarkerQuestionPoint): AddonQtypeDdMarkerQuestionPoint {
makePointProportional(point: CoreCoordinates): CoreCoordinates {
return {
x: Math.round(point.x * this.proportion),
y: Math.round(point.y * this.proportion),
@ -542,10 +550,10 @@ export class AddonQtypeDdMarkerQuestion {
* @return Coordinates.
*/
getDragXY(dragItem: HTMLElement): number[] {
const position = CoreDomUtils.getElementXY(dragItem, undefined, 'ddarea');
const position = this.getElementCoordinates(dragItem);
const bgImg = this.doc.bgImg();
if (bgImg) {
const bgImgXY = CoreDomUtils.getElementXY(bgImg, undefined, 'ddarea');
const bgImgXY = this.getElementCoordinates(bgImg);
position[0] -= bgImgXY[0];
position[1] -= bgImgXY[1];
@ -611,7 +619,7 @@ export class AddonQtypeDdMarkerQuestion {
});
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 dropZone?: SVGSVGElement;
/**
* Create the instance.
*
* @param instance Question instance.
* @param domUtils Dom Utils provider.
*/
constructor(protected instance: AddonQtypeDdMarkerQuestion) { }
/**
@ -60,20 +54,20 @@ export class AddonQtypeDdMarkerGraphicsApi {
const bgImg = this.instance.doc?.bgImg();
const dropZones = this.instance.doc?.topNode?.querySelector<HTMLElement>('div.ddarea div.dropzones');
const markerTexts = this.instance.doc?.markerTexts();
if (!bgImg || !dropZones || !markerTexts) {
const ddArea = this.instance.doc?.topNode?.querySelector<HTMLElement>('.ddarea');
if (!bgImg || !dropZones || !markerTexts || !ddArea) {
return;
}
const position = CoreDomUtils.getElementXY(bgImg, undefined, 'ddarea');
const position = CoreDomUtils.getRelativeElementPosition(bgImg, ddArea);
dropZones.style.left = position[0] + 'px';
dropZones.style.top = position[1] + 'px';
dropZones.style.left = position.x + 'px';
dropZones.style.top = position.y + 'px';
dropZones.style.width = bgImg.width + 'px';
dropZones.style.height = bgImg.height + 'px';
markerTexts.style.left = position[0] + 'px';
markerTexts.style.top = position[1] + 'px';
markerTexts.style.left = position.x + 'px';
markerTexts.style.top = position.y + 'px';
markerTexts.style.width = bgImg.width + 'px';
markerTexts.style.height = bgImg.height + 'px';

View File

@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and
// 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 { CoreUtils } from '@services/utils/utils';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreEventObserver } from '@singletons/events';
import { CoreLogger } from '@singletons/logger';
import { AddonModQuizDdwtosQuestionData } from '../component/ddwtos';
@ -26,7 +28,7 @@ export class AddonQtypeDdwtosQuestion {
protected logger: CoreLogger;
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 selected?: HTMLElement; // Selected element (being "dragged").
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.
* We clone these invisible elements to make the actual drag items.
*/
cloneDragItems(): void {
const dragHomes = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.dragHomes()));
async cloneDragItems(): Promise<void> {
const dragHomes = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.dragHomes()));
for (let x = 0; x < dragHomes.length; x++) {
this.cloneDragItemsForOneChoice(dragHomes[x]);
}
@ -110,7 +112,7 @@ export class AddonQtypeDdwtosQuestion {
*/
deselectDrags(): void {
// 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) => {
drag.classList.remove('selected');
});
@ -192,19 +194,13 @@ export class AddonQtypeDdwtosQuestion {
* Initialize the question.
*/
async initializer(): Promise<void> {
this.selectors = new AddonQtypeDdwtosQuestionCSSSelectors();
const container = <HTMLElement> this.container.querySelector(this.selectors.topNode());
if (this.readOnly) {
container.classList.add('readonly');
} else {
container.classList.add('notreadonly');
}
const container = this.container.querySelector<HTMLElement>(this.selectors.topNode());
container?.classList.add(this.readOnly ? 'readonly' : 'notreadonly');
// Wait for the elements to be ready.
await this.waitForReady();
this.setPaddingSizesAll();
await this.setPaddingSizesAll();
this.cloneDragItems();
this.initialPlaceOfDragItems();
this.makeDropZones();
@ -212,7 +208,7 @@ export class AddonQtypeDdwtosQuestion {
this.positionDragItems();
this.resizeListener = CoreDomUtils.onWindowResize(() => {
this.windowResized();
this.positionDragItems();
});
}
@ -220,7 +216,7 @@ export class AddonQtypeDdwtosQuestion {
* Initialize drag items, putting them in their initial place.
*/
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.
drags.forEach((drag) => {
@ -292,15 +288,15 @@ export class AddonQtypeDdwtosQuestion {
}
// 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) => {
this.makeDropZone(drop);
});
// 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;
if (!drag) {
// No element selected, nothing to do.
@ -379,33 +375,37 @@ export class AddonQtypeDdwtosQuestion {
return;
}
let position;
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) {
// Not placed, put it in home zone.
const groupNo = this.getGroup(drag) ?? -1;
const choiceNo = this.getChoice(drag) ?? -1;
position = CoreDomUtils.getElementXY(
this.container,
this.selectors.dragHome(groupNo, choiceNo),
'answercontainer',
);
drag.classList.add('unplaced');
const dragHome = this.container.querySelector<HTMLElement>(this.selectors.dragHome(groupNo, choiceNo));
if (dragHome) {
position = CoreDomUtils.getRelativeElementPosition(dragHome, parent);
}
} else {
// Get the drop zone position.
position = CoreDomUtils.getElementXY(
this.container,
this.selectors.dropForPlace(placeNo),
'addon-qtype-ddwtos-container',
);
drag.classList.remove('unplaced');
const dropZone = this.container.querySelector<HTMLElement>(this.selectors.dropForPlace(placeNo));
if (dropZone) {
position = CoreDomUtils.getRelativeElementPosition(dropZone, parent);
// Avoid the border.
position.x++;
position.y++;
}
}
drag.classList.toggle('unplaced', !placeNo);
if (position) {
drag.style.left = position[0] + 'px';
drag.style.top = position[1] + 'px';
drag.style.left = position.x + '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.
*/
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) => {
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 or if it took too long to load.
* @return Promise resolved when ready in the DOM.
*/
protected async waitForReady(retries: number = 0): Promise<void> {
const drag = <HTMLElement | null> Array.from(this.container.querySelectorAll(this.selectors.drags()))[0];
if (drag?.offsetParent || retries >= 10) {
// Ready or too many retries, stop.
return;
}
protected async waitForReady(): Promise<void> {
await CoreDomUtils.waitToBeInDOM(this.container);
const deferred = CoreUtils.promiseDefer<void>();
await CoreComponentsRegistry.waitComponentsReady(this.container, 'core-format-text', CoreFormatTextDirective);
setTimeout(async () => {
try {
await this.waitForReady(retries + 1);
} finally {
deferred.resolve();
}
}, 20);
const drag = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.dragHomes()))[0];
return deferred.promise;
await CoreDomUtils.waitToBeInDOM(drag);
}
/**
@ -452,7 +441,7 @@ export class AddonQtypeDdwtosQuestion {
*/
removeDragFromDrop(drag: HTMLElement): void {
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);
}
@ -473,9 +462,9 @@ export class AddonQtypeDdwtosQuestion {
/**
* Set the padding size for all groups.
*/
setPaddingSizesAll(): void {
async setPaddingSizesAll(): Promise<void> {
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.
*/
setPaddingSizeForGroup(groupNo: number): void {
const groupItems = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.dragHomesGroup(groupNo)));
async setPaddingSizeForGroup(groupNo: number): Promise<void> {
const groupItems = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.dragHomesGroup(groupNo)));
if (!groupItems.length) {
return;
}
await CoreDomUtils.waitToBeInDOM(groupItems[0]);
let maxWidth = 0;
let maxHeight = 0;
// Find max height and width.
groupItems.forEach((item) => {
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));
maxHeight = Math.max(maxHeight, Math.ceil(item.offsetHeight));
});
@ -507,21 +502,12 @@ export class AddonQtypeDdwtosQuestion {
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) => {
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 + '"]';
}
const buttonsContainer = <HTMLElement> header.querySelector(selector);
const buttonsContainer = header.querySelector<HTMLIonButtonsElement>(selector);
if (buttonsContainer) {
this.mergeContextMenus(buttonsContainer);
@ -147,7 +147,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
*
* @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.
const secondaryContextMenu = this.element.querySelector('core-context-menu');
if (!secondaryContextMenu) {
@ -194,60 +194,26 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
/**
* Search the ion-header where the buttons should be added.
*
* @param retries Number of retries so far.
* @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;
while (parentPage) {
if (!parentPage.parentElement) {
// No parent, stop.
break;
}
while (parentPage && parentPage.parentElement) {
// Get the next parent 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.
const header = this.searchHeaderInPage(parentPage);
if (header && getComputedStyle(header, null).display != 'none') {
const header = parentPage?.querySelector<HTMLIonHeaderElement>(':scope > ion-header');
if (header && getComputedStyle(header).display !== 'none') {
return header;
}
}
}
// Header not found.
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.
// Header not found, reject.
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.
*/
@ -280,7 +246,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
}
/**
* Component destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
// This component was destroyed, remove all the buttons that were moved.

View File

@ -1,4 +1,6 @@
<ng-content></ng-content>
<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>
<ion-button fill="clear" [attr.aria-label]="label | translate" core-suppress-events (onClick)="toggle($event)"
(mousedown)="doNotBlur($event)" (keydown)="doNotBlur($event)" (keyup)="toggle($event)">
<ion-icon [name]=" iconName" slot="icon-only" aria-hidden="true">
</ion-icon>
</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.
@ContentChild(IonInput) ionInput?: IonInput;
shown!: boolean; // Whether the password is shown.
label!: string; // Label for the button to show/hide.
iconName!: string; // Name of the icon of the button to show/hide.
selector = ''; // Selector to identify the input.
shown = false; // Whether the password is shown.
label = ''; // Label for the button to show/hide.
iconName = ''; // Name of the icon of the button to show/hide.
protected input?: HTMLInputElement | null; // Input affected.
protected input?: HTMLInputElement; // Input affected.
protected element: HTMLElement; // Current element.
constructor(element: ElementRef) {
@ -57,38 +56,32 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
}
/**
* Component being initialized.
* @inheritdoc
*/
ngOnInit(): void {
this.shown = CoreUtils.isTrueOrOne(this.initialShown);
this.selector = 'input[name="' + this.name + '"]';
this.setData();
}
/**
* View has been initialized.
* @inheritdoc
*/
ngAfterViewInit(): void {
this.searchInput();
}
/**
* Search the input to show/hide.
*/
protected async searchInput(): Promise<void> {
async ngAfterViewInit(): Promise<void> {
if (this.ionInput) {
// It's an ion-input, use it to get the native element.
this.input = await this.ionInput.getInputElement();
this.setData(this.input);
return;
}
// 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) {
// Input found. Set the right type.
this.input.type = this.shown ? 'text' : 'password';
if (!this.input) {
return;
}
this.setData(this.input);
// By default, don't autocapitalize and autocorrect.
if (!this.input.getAttribute('autocorrect')) {
@ -98,17 +91,16 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
this.input.setAttribute('autocapitalize', 'none');
}
}
}
/**
* 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.iconName = this.shown ? 'fas-eye-slash' : 'fas-eye';
if (this.input) {
this.input.type = this.shown ? 'text' : 'password';
}
input.type = this.shown ? 'text' : 'password';
}
/**
@ -117,20 +109,49 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
* @param event The mouse event.
*/
toggle(event: Event): void {
if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
return;
}
event.preventDefault();
event.stopPropagation();
const isFocused = document.activeElement === this.input;
this.shown = !this.shown;
this.setData();
if (isFocused && CoreApp.isAndroid()) {
// In Android, the keyboard is closed when the input type changes. Focus it again.
setTimeout(() => {
CoreDomUtils.focusElement(this.input!);
}, 400);
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()) {
CoreDomUtils.focusElement(this.input);
}
}
/**
* Do not loose focus.
*
* @param event The mouse event.
*/
doNotBlur(event: Event): void {
if (event.type == 'keydown' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
return;
}
event.preventDefault();
event.stopPropagation();
}
/**
* Checks if Space or Enter have been pressed.
*
* @param event Keyboard Event.
* @returns Wether space or enter have been pressed.
*/
protected isValidKeyboardKey(event: KeyboardEvent): boolean {
return event.key == ' ' || event.key == 'Enter';
}
}

View File

@ -32,7 +32,7 @@ export class CoreAutoFocusDirective implements AfterViewInit {
@Input('core-auto-focus') autoFocus: boolean | string = true;
protected element: HTMLElement;
protected element: HTMLIonInputElement | HTMLIonTextareaElement | HTMLIonSearchbarElement | HTMLElement;
constructor(element: ElementRef) {
this.element = element.nativeElement;
@ -41,51 +41,27 @@ export class CoreAutoFocusDirective implements AfterViewInit {
/**
* @inheritdoc
*/
ngAfterViewInit(): void {
async ngAfterViewInit(): Promise<void> {
if (CoreUtils.isFalseOrZero(this.autoFocus)) {
return;
}
this.setFocus();
await CoreDomUtils.waitToBeInDOM(this.element);
let focusElement = this.element;
if ('getInputElement' in focusElement) {
// If it's an Ionic element get the right input to use.
focusElement.componentOnReady && await focusElement.componentOnReady();
focusElement = await focusElement.getInputElement();
}
/**
* Function to focus the element.
*
* @param retries Internal param to stop retrying on 0.
*/
protected setFocus(retries = 10): void {
if (retries == 0) {
if (!focusElement) {
return;
}
// Wait a bit to make sure the view is loaded.
setTimeout(() => {
// If it's a ion-input or ion-textarea, search the right input to use.
let element: HTMLElement | null = null;
CoreDomUtils.focusElement(focusElement);
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();
CoreDomUtils.onElementSlot(this.element, () => {
this.calculateHeight();
});
this.listenScrollEvents();
}
@ -146,7 +150,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
? this.finalHeight
: this.initialHeight;
this.setBarHeight(newHeight); }
this.setBarHeight(newHeight);
}
});
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 { CoreLoadingComponent } from '@components/loading/loading';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreEventObserver } from '@singletons/events';
@ -50,11 +51,14 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
protected expandedHeight = 0;
protected resizeListener?: CoreEventObserver;
protected domPromise?: CoreCancellablePromise<void>;
protected uniqueId: string;
constructor(el: ElementRef<HTMLElement>) {
this.element = el.nativeElement;
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');
toggleButton.classList.add('collapsible-toggle');
toggleButton.setAttribute('fill', 'clear');
toggleButton.setAttribute('aria-controls', this.uniqueId);
const toggleText = document.createElement('span');
toggleText.classList.add('collapsible-toggle-text');
@ -163,7 +168,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
expandArrow.classList.add('collapsible-toggle-arrow');
toggleButton.appendChild(expandArrow);
this.element.appendChild(toggleButton);
this.element.append(toggleButton);
this.toggleExpand(this.expanded);
}
@ -194,6 +199,9 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
expand = !this.expanded;
}
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.setHeight(!expand ? this.maxHeight: undefined);

View File

@ -13,7 +13,8 @@
// limitations under the License.
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.
@ -30,6 +31,7 @@ export class CoreFabDirective implements OnInit, OnDestroy {
protected element: HTMLElement;
protected content?: HTMLIonContentElement | null;
protected initialPaddingBottom = 0;
protected domPromise?: CoreCancellablePromise<void>;
constructor(el: ElementRef) {
this.element = el.nativeElement;
@ -39,15 +41,31 @@ export class CoreFabDirective implements OnInit, OnDestroy {
/**
* @inheritdoc
*/
async ngOnInit(retries = 3): Promise<void> {
this.content = this.element.closest('ion-content');
if (!this.content) {
if(retries > 0) {
await CoreUtils.nextTicks(50);
async ngOnInit(): Promise<void> {
this.domPromise = CoreDomUtils.waitToBeInDOM(this.element);
await this.domPromise;
this.ngOnInit(retries - 1);
this.content = this.element.closest('ion-content');
if (!this.content) {
return;
}
// Add space at the bottom to let the user see the whole content.
this.initialPaddingBottom = parseFloat(this.content.style.getPropertyValue('--padding-bottom') || '0');
await this.calculatePlace();
CoreDomUtils.onElementSlot(this.element, () => {
this.calculatePlace();
});
}
/**
* Calculate the height of the footer.
*/
protected async calculatePlace(): Promise<void> {
if (!this.content) {
return;
}
@ -58,8 +76,6 @@ export class CoreFabDirective implements OnInit, OnDestroy {
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');
}
@ -70,6 +86,7 @@ export class CoreFabDirective implements OnInit, OnDestroy {
if (this.content) {
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) == '#') {
// Look for id or name.
href = href.substring(1);
CoreDomUtils.scrollToElementBySelector(
this.element.closest('ion-content'),
this.content,
const container = this.element.closest('ion-content');
if (container) {
CoreDomUtils.scrollViewToElement(
container,
`#${href}, [name='${href}']`,
);
}
return;
}

View File

@ -34,6 +34,7 @@ import {
CoreCourseProvider,
} from '@features/course/services/course';
import {
CoreCourseHelper,
CoreCourseSection,
} from '@features/course/services/course-helper';
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);
}
if (module.uservisible !== false && module.handlerData?.action) {
if (CoreCourseHelper.canUserViewModule(module, section) && module.handlerData?.action) {
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.
const moduleIdToScroll = this.moduleId && previousValue === undefined ? this.moduleId : moduleId;
if (moduleIdToScroll) {
setTimeout(() => {
this.scrollToModule(moduleIdToScroll);
}, 200);
} else {
this.content.scrollToTop(0);
}
@ -512,9 +511,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* @param moduleId Module ID.
*/
protected scrollToModule(moduleId: number): void {
CoreDomUtils.scrollToElementBySelector(
CoreDomUtils.scrollViewToElement(
this.elementRef.nativeElement,
this.content,
'#core-course-module-' + moduleId,
);
}
@ -574,7 +572,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
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) {
break;
@ -632,8 +631,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* @return Whether the section can be viewed.
*/
canViewSection(section: CoreCourseSection): boolean {
return section.uservisible !== false && !section.hiddenbynumsections &&
section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
return CoreCourseHelper.canUserViewSection(section) && !CoreCourseHelper.isSectionStealth(section);
}
}

View File

@ -12,16 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { Component, ElementRef, Input, OnInit } from '@angular/core';
import {
CoreCourseModuleCompletionStatus,
CoreCourseModuleCompletionTracking,
CoreCourseProvider,
} 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 { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import { IonContent } from '@ionic/angular';
import { CoreDomUtils } from '@services/utils/dom';
import { ModalController } from '@singletons';
@ -35,8 +34,6 @@ import { ModalController } from '@singletons';
})
export class CoreCourseCourseIndexComponent implements OnInit {
@ViewChild(IonContent) content?: IonContent;
@Input() sections: CoreCourseSection[] = [];
@Input() selectedId?: number;
@Input() course?: CoreCourseAnyCourseData;
@ -77,11 +74,10 @@ export class CoreCourseCourseIndexComponent implements OnInit {
// Clone sections to add information.
this.sectionsToRender = this.sections
.filter((section) => !section.hiddenbynumsections &&
section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID)
.filter((section) => !CoreCourseHelper.isSectionStealth(section))
.map((section) => {
const modules = section.modules
.filter((module) => module.visibleoncoursepage !== 0 && !module.noviewlink)
.filter((module) => !CoreCourseHelper.isModuleStealth(module, section) && !module.noviewlink)
.map((module) => {
const completionStatus = !completionEnabled || module.completiondata === undefined ||
module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE
@ -93,7 +89,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
name: module.name,
course: module.course,
visible: !!module.visible,
uservisible: !!module.uservisible,
uservisible: CoreCourseHelper.canUserViewModule(module, section),
completionStatus,
};
});
@ -103,7 +99,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
name: section.name,
availabilityinfo: !!section.availabilityinfo,
visible: !!section.visible,
uservisible: section.uservisible !== false,
uservisible: CoreCourseHelper.canUserViewSection(section),
expanded: section.id === this.selectedId,
highlighted: currentSectionData.section.id === section.id,
hasVisibleModules: modules.length > 0,
@ -113,13 +109,10 @@ export class CoreCourseCourseIndexComponent implements OnInit {
this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
setTimeout(() => {
CoreDomUtils.scrollToElementBySelector(
CoreDomUtils.scrollViewToElement(
this.elementRef.nativeElement,
this.content,
'.item.item-current',
);
}, 300);
}
/**

View File

@ -21,7 +21,7 @@
</ion-label>
</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">
<ion-icon name="fas-edit" [attr.aria-label]="'core.course.completion_automatic:todo' | translate "></ion-icon>
<ion-label>
@ -42,7 +42,7 @@
</div>
<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-label>
{{ 'core.course.todo' | translate }}

View File

@ -13,8 +13,8 @@
// limitations under the License.
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { CoreCourse, CoreCourseProvider, CoreCourseWSSection } from '@features/course/services/course';
import { CoreCourseModuleData } from '@features/course/services/course-helper';
import { CoreCourse, CoreCourseWSSection } from '@features/course/services/course';
import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { IonContent } from '@ionic/angular';
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.
*/
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.
*/
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',
};
if (module.uservisible === false) {
if (!CoreCourseHelper.canUserViewModule(module)) {
const section = next ? this.nextModuleSection : this.previousModuleSection;
options.params = {
module,

View File

@ -35,10 +35,10 @@
</ion-chip>
<!-- 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 }}
</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 }}
</ion-badge>
</div>

View File

@ -19,6 +19,7 @@ import {
CoreCourseModuleData,
CoreCourseModuleCompletionData,
CoreCourseSection,
CoreCourseHelper,
} from '@features/course/services/course-helper';
import { CoreCourse, CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking } from '@features/course/services/course';
import { CoreCourseModuleDelegate, CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate';
@ -166,7 +167,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
* @param event Click event.
*/
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);
}
}

View File

@ -75,7 +75,7 @@ export class CoreCourseListModTypePage implements OnInit {
}
section.modules = section.modules.filter((mod) => {
if (mod.uservisible === false || !CoreCourse.moduleHasView(mod)) {
if (!CoreCourseHelper.canUserViewModule(mod, section) || !CoreCourse.moduleHasView(mod)) {
// Ignore this module.
return false;
}

View File

@ -33,10 +33,10 @@
</ion-chip>
<!-- 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 }}
</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 }}
</ion-badge>
</div>

View File

@ -198,7 +198,7 @@ export class CoreCourseHelperProvider {
}
// Check if the module is stealth.
module.isStealth = module.visibleoncoursepage === 0 || (!!module.visible && !section.visible);
module.isStealth = CoreCourseHelper.isModuleStealth(module, section);
}));
return section;
@ -208,6 +208,50 @@ export class CoreCourseHelperProvider {
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.
*

View File

@ -33,7 +33,7 @@ import { CoreError } from '@classes/errors/error';
import { CoreWSFile, CoreWSExternalWarning } from '@services/ws';
import { CHECK_UPDATES_TIMES_TABLE, CoreCourseCheckUpdatesDBRecord } from './database/module-prefetch';
import { CoreFileSizeSum } from '@services/plugin-file-delegate';
import { CoreCourseModuleData } from './course-helper';
import { CoreCourseHelper, CoreCourseModuleData } from './course-helper';
const ROOT_CACHE_KEY = 'mmCourse:';
@ -956,7 +956,7 @@ export class CoreCourseModulePrefetchDelegateService extends CoreDelegate<CoreCo
* @return Promise resolved with true if downloadable, false otherwise.
*/
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.
return false;
}

View File

@ -20,7 +20,6 @@ import {
ViewChild,
ElementRef,
OnInit,
AfterContentInit,
OnDestroy,
Optional,
AfterViewInit,
@ -56,7 +55,7 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise';
templateUrl: 'core-editor-rich-text-editor.html',
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/
// @todo: Anchor button, fullscreen...
@ -149,7 +148,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
}
/**
* Component being initialized.
* @inheritdoc
*/
ngOnInit(): void {
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();
await this.waitLoadingsDone();
@ -202,13 +201,14 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
this.deleteDraftOnSubmitOrCancel();
}
}
/**
* @inheritdoc
*/
async ngAfterViewInit(): Promise<void> {
const label = this.element.closest('ion-item')?.querySelector('ion-label');
const ionItem = this.element.closest<HTMLIonItemElement>('ion-item');
if (!ionItem) {
return;
}
ionItem.classList.add('item-rte');
const label = ionItem.querySelector('ion-label');
if (!label) {
return;
@ -773,7 +773,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
* @param event Event.
*/
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.stopPropagation();

View File

@ -30,6 +30,7 @@ import { makeSingleton } from '@singletons';
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
import { CoreLogger } from '@singletons/logger';
import { CoreSite } from '@classes/site';
import { CoreCourseHelper } from '@features/course/services/course-helper';
/**
* Helper service to provide filter functionalities.
@ -159,7 +160,7 @@ export class CoreFilterHelperProvider {
sections.forEach((section) => {
if (section.modules) {
section.modules.forEach((module) => {
if (module.uservisible) {
if (CoreCourseHelper.canUserViewModule(module, section)) {
contexts.push({
contextlevel: 'module',
instanceid: module.id,

View File

@ -13,8 +13,8 @@
// limitations under the License.
import { ActivatedRoute } from '@angular/router';
import { AfterViewInit, Component, ElementRef, OnDestroy, Optional } from '@angular/core';
import { IonContent, IonRefresher } from '@ionic/angular';
import { AfterViewInit, Component, ElementRef, OnDestroy } from '@angular/core';
import { IonRefresher } from '@ionic/angular';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreGrades } from '@features/grades/services/grades';
@ -59,7 +59,6 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
constructor(
protected route: ActivatedRoute,
protected element: ElementRef<HTMLElement>,
@Optional() protected content?: IonContent,
) {
try {
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId', { route });
@ -170,11 +169,9 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
if (row) {
this.toggleRow(row, true);
await CoreUtils.nextTick();
CoreDomUtils.scrollToElementBySelector(
CoreDomUtils.scrollViewToElement(
this.element.nativeElement,
this.content,
'#grade-' + row.id,
);
this.gradeId = undefined;

View File

@ -14,7 +14,7 @@
import { Component, ViewChild, ElementRef, OnInit, ChangeDetectorRef } from '@angular/core';
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 { CoreDomUtils } from '@services/utils/dom';
@ -46,7 +46,6 @@ import { CoreText } from '@singletons/text';
})
export class CoreLoginEmailSignupPage implements OnInit {
@ViewChild(IonContent) content?: IonContent;
@ViewChild(CoreRecaptchaComponent) recaptchaComponent?: CoreRecaptchaComponent;
@ViewChild('ageForm') ageFormElement?: ElementRef;
@ViewChild('signupFormEl') signupFormElement?: ElementRef;
@ -285,9 +284,8 @@ export class CoreLoginEmailSignupPage implements OnInit {
this.changeDetector.detectChanges();
// Scroll to the first element with errors.
const errorFound = CoreDomUtils.scrollToInputError(
const errorFound = await CoreDomUtils.scrollViewToInputError(
this.elementRef.nativeElement,
this.content,
);
if (!errorFound) {

View File

@ -16,7 +16,7 @@ import { Type } from '@angular/core';
import { CoreConstants } from '@/core/constants';
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 { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index';
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.
const method = this.handlerSchema.coursepagemethod;
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.
* 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.
*/
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);
}, debounceDelay);
@ -461,15 +510,30 @@ export class CoreDomUtilsProvider {
/**
* Focus an element and open keyboard.
*
* @param el HTML element to focus.
* @param focusElement HTML element to focus.
*/
focusElement(el: HTMLElement): void {
if (el?.focus) {
el.focus();
if (CoreApp.isAndroid() && this.supportsInputKeyboard(el)) {
async focusElement(focusElement: HTMLElement): Promise<void> {
let retries = 10;
if (!focusElement.focus) {
throw new CoreError('Element to focus cannot be focused');
}
while (retries > 0 && focusElement !== document.activeElement) {
focusElement.focus();
if (focusElement === document.activeElement) {
await CoreUtils.nextTick();
if (CoreApp.isAndroid() && this.supportsInputKeyboard(focusElement)) {
// On some Android versions the keyboard doesn't open automatically.
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.
*
* @param container Element to search in.
* @param element Element to search in.
* @param selector Selector to find the element to gets the position.
* @param positionParentClass Parent Class where to stop calculating the position. Default inner-scroll.
* @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(container: HTMLElement, selector: string, positionParentClass?: string): number[] | null;
getElementXY(container: HTMLElement, selector?: string, positionParentClass?: string): number[] | null {
let element: HTMLElement | null = <HTMLElement> (selector ? container.querySelector(selector) : container);
let positionTop = 0;
let positionLeft = 0;
if (!positionParentClass) {
positionParentClass = 'inner-scroll';
}
if (!element) {
getElementXY(element: HTMLElement, selector?: string, positionParentClass = 'inner-scroll'): [number, number] | null {
if (selector) {
const foundElement = element.querySelector<HTMLElement>(selector);
if (!foundElement) {
// Element not found.
return null;
}
while (element) {
positionLeft += (element.offsetLeft - element.scrollLeft + element.clientLeft);
positionTop += (element.offsetTop - element.scrollTop + element.clientTop);
const offsetElement = element.offsetParent;
element = element.parentElement;
// Every parent class has to be checked but the position has to be got form offsetParent.
while (offsetElement != element && element) {
// If positionParentClass element is reached, stop adding tops.
if (element.className.indexOf(positionParentClass) != -1) {
element = null;
} else {
element = element.parentElement;
}
element = foundElement;
}
// Finally, check again.
if (element?.className.indexOf(positionParentClass) != -1) {
element = null;
}
const parent = element.closest<HTMLElement>(`.${positionParentClass}`);
if (!parent) {
return null;
}
return [positionLeft, positionTop];
const position = CoreDomUtils.getRelativeElementPosition(element, parent);
// Calculate the top and left positions.
return [
Math.ceil(position.x),
Math.ceil(position.y),
];
}
/**
* Retrieve the position of a element relative to another element.
*
* @param element Element to get the position.
* @param parent Parent element to get relative position.
* @return X and Y position.
*/
getRelativeElementPosition(element: HTMLElement, parent: HTMLElement): CoreCoordinates {
// Get the top, left coordinates of two elements
const elementRectangle = element.getBoundingClientRect();
const parentRectangle = parent.getBoundingClientRect();
// Calculate the top and left positions.
return {
x: elementRectangle.x - parentRectangle.x,
y: elementRectangle.y - parentRectangle.y,
};
}
/**
@ -1032,13 +1100,11 @@ export class CoreDomUtilsProvider {
* @param selector Selector to search.
*/
removeElement(element: HTMLElement, selector: string): void {
if (element) {
const selected = element.querySelector(selector);
if (selected) {
selected.remove();
}
}
}
/**
* Search and remove a certain element from an HTML code.
@ -1134,9 +1200,9 @@ export class CoreDomUtilsProvider {
}
// Treat video posters.
if (media.tagName == 'VIDEO' && media.getAttribute('poster')) {
const currentPoster = media.getAttribute('poster');
const newPoster = paths[CoreTextUtils.decodeURIComponent(currentPoster!)];
if (media.tagName == 'VIDEO' && currentPoster) {
const newPoster = paths[CoreTextUtils.decodeURIComponent(currentPoster)];
if (newPoster !== undefined) {
media.setAttribute('poster', newPoster);
}
@ -1173,8 +1239,8 @@ export class CoreDomUtilsProvider {
* @return Returns a promise which is resolved when the scroll has completed.
* @deprecated since 3.9.5. Use directly the IonContent class.
*/
scrollTo(content: IonContent, x: number, y: number, duration?: number): Promise<void> {
return content.scrollToPoint(x, y, duration || 0);
scrollTo(content: IonContent, x: number, y: number, duration = 0): Promise<void> {
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.
* @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);
}
@ -1244,7 +1310,7 @@ export class CoreDomUtilsProvider {
const scrollElement = await content.getScrollElement();
return scrollElement.scrollTop || 0;
} catch (error) {
} catch {
return 0;
}
}
@ -1252,51 +1318,34 @@ export class CoreDomUtilsProvider {
/**
* Scroll to a certain element.
*
* @param content The content that must be scrolled.
* @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.
* @return True if the element is found, false otherwise.
* @return Wether the scroll suceeded.
*/
scrollToElement(content: IonContent, element: HTMLElement, scrollParentClass?: string, duration?: number): boolean {
const position = this.getElementXY(element, undefined, scrollParentClass);
if (!position) {
async scrollViewToElement(element: HTMLElement, selector?: string, duration = 0): Promise<boolean> {
await CoreDomUtils.waitToBeInDOM(element);
if (selector) {
const foundElement = element.querySelector<HTMLElement>(selector);
if (!foundElement) {
// Element not found.
return false;
}
content.scrollToPoint(position[0], position[1], duration || 0);
return true;
element = foundElement;
}
/**
* 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) {
const content = element.closest<HTMLIonContentElement>('ion-content') ?? undefined;
if (!content) {
// Content to scroll, not found.
return false;
}
try {
const position = this.getElementXY(container, selector, scrollParentClass);
if (!position) {
return false;
}
const position = CoreDomUtils.getRelativeElementPosition(element, content);
content.scrollToPoint(position[0], position[1], duration || 0);
await content.scrollToPoint(position.x, position.y, duration);
return true;
} catch {
@ -1308,12 +1357,71 @@ export class CoreDomUtilsProvider {
* 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 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.
*/
scrollToInputError(container: HTMLElement | null, content?: IonContent, scrollParentClass?: string): boolean {
return this.scrollToElementBySelector(container, content, '.core-input-error', scrollParentClass);
async scrollViewToInputError(container: HTMLElement): Promise<boolean> {
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.
* Use of this function is discouraged. Please use onWindowResize to check window resize event.
*
* @param windowWidth Initial window width.
* @param windowHeight Initial window height.
@ -2318,3 +2427,11 @@ export enum VerticalPoint {
MID = 'mid',
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.
*
* @param form Form element.
* @param formRef Form element.
* @param siteId The site affected. If not provided, no site affected.
*/
static triggerFormCancelledEvent(formRef?: ElementRef | HTMLFormElement | undefined, siteId?: string): void {
@ -77,7 +77,7 @@ export class CoreForms {
/**
* Trigger form submitted event.
*
* @param form Form element.
* @param formRef Form element.
* @param online Whether the action was done in offline or not.
* @param siteId The site affected. If not provided, no site affected.
*/

View File

@ -72,7 +72,7 @@
&:before {
content: '';
height: 60px;
height: 100%;
position: absolute;
@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));

View File

@ -985,6 +985,18 @@ ion-chip {
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 {
@ -1243,7 +1255,7 @@ ion-datetime.datetime-disabled {
// Make links clickable when inside radio or checkbox items. Style part.
@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);
&::after {
@ -1264,7 +1276,7 @@ ion-datetime.datetime-disabled {
// 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.
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) {
cursor: pointer;
ion-label {