MOBILE-3810 format-text: Remove format text full view and review toggle

main
Pau Ferrer Ocaña 2021-11-17 11:46:37 +01:00
parent dace9898d0
commit f31fc5a6df
16 changed files with 212 additions and 230 deletions

View File

@ -1,4 +1,3 @@
<core-dynamic-component [component]="pluginComponent" [data]="data"> <core-dynamic-component [component]="pluginComponent" [data]="data">
<!-- This content will be replaced by the component if found. --> <!-- This content will be replaced by the component if found. -->
<core-loading [hideUntil]="pluginLoaded"> <core-loading [hideUntil]="pluginLoaded">
@ -9,9 +8,8 @@
{{ 'addon.mod_assign.feedbacknotsupported' | translate }} {{ 'addon.mod_assign.feedbacknotsupported' | translate }}
</ion-badge> </ion-badge>
<p *ngIf="text"> <p *ngIf="text">
<core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" <core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="120" [text]="text"
[fullTitle]="plugin.name" [text]="text" contextLevel="module" [contextInstanceId]="assign.cmid" contextLevel="module" [contextInstanceId]="assign.cmid" [courseId]="assign.course">
[courseId]="assign.course">
</core-format-text> </core-format-text>
</p> </p>
<core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid"

View File

@ -1,4 +1,3 @@
<core-dynamic-component [component]="pluginComponent" [data]="data"> <core-dynamic-component [component]="pluginComponent" [data]="data">
<!-- This content will be replaced by the component if found. --> <!-- This content will be replaced by the component if found. -->
<core-loading [hideUntil]="pluginLoaded"> <core-loading [hideUntil]="pluginLoaded">
@ -9,9 +8,8 @@
{{ 'addon.mod_assign.submissionnotsupported' | translate }} {{ 'addon.mod_assign.submissionnotsupported' | translate }}
</ion-badge> </ion-badge>
<p *ngIf="text"> <p *ngIf="text">
<core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" <core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="120" [text]="text"
[fullTitle]="plugin.name" [text]="text" contextLevel="module" [contextInstanceId]="assign.cmid" contextLevel="module" [contextInstanceId]="assign.cmid" [courseId]="assign.course">
[courseId]="assign.course">
</core-format-text> </core-format-text>
</p> </p>
<core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid"

View File

@ -3,16 +3,14 @@
<ion-label> <ion-label>
<h2>{{ plugin.name }}</h2> <h2>{{ plugin.name }}</h2>
<p> <p>
<core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" <core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="120" [text]="text" contextLevel="module"
[fullTitle]="plugin.name" [text]="text" contextLevel="module" [contextInstanceId]="assign.cmid" [contextInstanceId]="assign.cmid" [courseId]="assign.course">
[courseId]="assign.course">
</core-format-text> </core-format-text>
</p> </p>
</ion-label> </ion-label>
<div slot="end"> <div slot="end">
<div class="ion-text-end"> <div class="ion-text-end">
<ion-button fill="clear" *ngIf="canEdit" (click)="editComment()" color="dark" <ion-button fill="clear" *ngIf="canEdit" (click)="editComment()" color="dark" [attr.aria-label]="'core.edit' | translate">
[attr.aria-label]="'core.edit' | translate">
<ion-icon name="fas-pen" slot="icon-only" aria-hidden="true"></ion-icon> <ion-icon name="fas-pen" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
</div> </div>
@ -25,9 +23,8 @@
<!-- Edit --> <!-- Edit -->
<ion-item class="ion-text-wrap" *ngIf="edit && loaded"> <ion-item class="ion-text-wrap" *ngIf="edit && loaded">
<ion-label class="sr-only">{{ plugin.name }}</ion-label> <ion-label class="sr-only">{{ plugin.name }}</ion-label>
<core-rich-text-editor [control]="control" [placeholder]="plugin.name" <core-rich-text-editor [control]="control" [placeholder]="plugin.name" name="assignfeedbackcomments_editor" [component]="component"
name="assignfeedbackcomments_editor" [component]="component" [componentId]="assign.cmid" [autoSave]="true" [componentId]="assign.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="assign.cmid"
contextLevel="module" [contextInstanceId]="assign.cmid" elementId="assignfeedbackcomments_editor" elementId="assignfeedbackcomments_editor" [draftExtraParams]="{userid: userId, action: 'grade'}">
[draftExtraParams]="{userid: userId, action: 'grade'}">
</core-rich-text-editor> </core-rich-text-editor>
</ion-item> </ion-item>

View File

@ -4,9 +4,8 @@
<h2>{{ plugin.name }}</h2> <h2>{{ plugin.name }}</h2>
<p *ngIf="words">{{ 'addon.mod_assign.numwords' | translate: {'$a': words} }}</p> <p *ngIf="words">{{ 'addon.mod_assign.numwords' | translate: {'$a': words} }}</p>
<p> <p>
<core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" <core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="120" [text]="text" contextLevel="module"
[fullTitle]="plugin.name" [text]="text" contextLevel="module" [contextInstanceId]="assign.cmid" [contextInstanceId]="assign.cmid" [courseId]="assign.course">
[courseId]="assign.course">
</core-format-text> </core-format-text>
</p> </p>
</ion-label> </ion-label>
@ -15,7 +14,9 @@
<!-- Edit --> <!-- Edit -->
<div *ngIf="edit && loaded"> <div *ngIf="edit && loaded">
<ion-item-divider class="ion-text-wrap" sticky="true"> <ion-item-divider class="ion-text-wrap" sticky="true">
<ion-label><h2>{{ plugin.name }}</h2></ion-label> <ion-label>
<h2>{{ plugin.name }}</h2>
</ion-label>
</ion-item-divider> </ion-item-divider>
<ion-item class="ion-text-wrap" *ngIf="wordLimitEnabled && words >= 0"> <ion-item class="ion-text-wrap" *ngIf="wordLimitEnabled && words >= 0">
<ion-label> <ion-label>
@ -25,10 +26,10 @@
</ion-item> </ion-item>
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label class="sr-only">{{ plugin.name }}</ion-label> <ion-label class="sr-only">{{ plugin.name }}</ion-label>
<core-rich-text-editor [control]="control" [placeholder]="plugin.name" <core-rich-text-editor [control]="control" [placeholder]="plugin.name" name="onlinetext_editor_text"
name="onlinetext_editor_text" (contentChanged)="onChange($event)" [component]="component" (contentChanged)="onChange($event)" [component]="component" [componentId]="assign.cmid" [autoSave]="true" contextLevel="module"
[componentId]="assign.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="assign.cmid" [contextInstanceId]="assign.cmid" elementId="onlinetext_editor"
elementId="onlinetext_editor" [draftExtraParams]="{userid: currentUserId, action: 'editsubmission'}"> [draftExtraParams]="{userid: currentUserId, action: 'editsubmission'}">
</core-rich-text-editor> </core-rich-text-editor>
</ion-item> </ion-item>
</div> </div>

View File

@ -87,7 +87,7 @@
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label> <ion-label>
<h2>{{ 'addon.mod_workshop.conclusion' | translate }}</h2> <h2>{{ 'addon.mod_workshop.conclusion' | translate }}</h2>
<core-format-text fullOnClick="true" [component]="component" [componentId]="module.id" [text]="workshop!.conclusion" <core-format-text [maxHeight]="120" [component]="component" [componentId]="module.id" [text]="workshop!.conclusion"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
@ -120,8 +120,8 @@
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label> <ion-label>
<h2>{{ 'addon.mod_workshop.areainstructauthors' | translate }}</h2> <h2>{{ 'addon.mod_workshop.areainstructauthors' | translate }}</h2>
<core-format-text fullOnClick="true" [component]="component" [componentId]="module.id" <core-format-text [maxHeight]="120" [component]="component" [componentId]="module.id" [text]="workshop!.instructauthors"
[text]="workshop!.instructauthors" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
</ion-item> </ion-item>
@ -184,7 +184,7 @@
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label> <ion-label>
<h2>{{ 'addon.mod_workshop.areainstructreviewers' | translate }}</h2> <h2>{{ 'addon.mod_workshop.areainstructreviewers' | translate }}</h2>
<core-format-text fullOnClick="true" [component]="component" [componentId]="module.id" <core-format-text [maxHeight]="120" [component]="component" [componentId]="module.id"
[text]="workshop!.instructreviewers" contextLevel="module" [contextInstanceId]="module.id" [text]="workshop!.instructreviewers" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="courseId"> [courseId]="courseId">
</core-format-text> </core-format-text>

View File

@ -61,8 +61,6 @@ export class CoreFormatTextDirective implements OnChanges {
@Input() adaptImg?: boolean | string = true; // Whether to adapt images to screen width. @Input() adaptImg?: boolean | string = true; // Whether to adapt images to screen width.
@Input() clean?: boolean | string; // Whether all the HTML tags should be removed. @Input() clean?: boolean | string; // Whether all the HTML tags should be removed.
@Input() singleLine?: boolean | string; // Whether new lines should be removed (all text in single line). Only if clean=true. @Input() singleLine?: boolean | string; // Whether new lines should be removed (all text in single line). Only if clean=true.
@Input() fullOnClick?: boolean | string; // Whether it should open a new page with the full contents on click.
@Input() fullTitle?: string; // Title to use in full view. Defaults to "Description".
@Input() highlight?: string; // Text to highlight. @Input() highlight?: string; // Text to highlight.
@Input() filter?: boolean | string; // Whether to filter the text. If not defined, true if contextLevel and instanceId are set. @Input() filter?: boolean | string; // Whether to filter the text. If not defined, true if contextLevel and instanceId are set.
@Input() contextLevel?: string; // The context level of the text. @Input() contextLevel?: string; // The context level of the text.
@ -73,6 +71,8 @@ export class CoreFormatTextDirective implements OnChanges {
@Input() openLinksInApp?: boolean; // Whether links should be opened in InAppBrowser. @Input() openLinksInApp?: boolean; // Whether links should be opened in InAppBrowser.
@Input() hideIfEmpty = false; // If true, the tag will contain nothing if text is empty. @Input() hideIfEmpty = false; // If true, the tag will contain nothing if text is empty.
@Input() fullOnClick?: boolean | string; // @deprecated on 4.0 Won't do anything.
@Input() fullTitle?: string; // @deprecated on 4.0 Won't do anything..
/** /**
* Max height in pixels to render the content box. It should be 50 at least to make sense. * Max height in pixels to render the content box. It should be 50 at least to make sense.
* Using this parameter will force display: block to calculate height better. * Using this parameter will force display: block to calculate height better.
@ -84,10 +84,11 @@ export class CoreFormatTextDirective implements OnChanges {
@Output() onClick: EventEmitter<void> = new EventEmitter(); // Called when clicked. @Output() onClick: EventEmitter<void> = new EventEmitter(); // Called when clicked.
protected element: HTMLElement; protected element: HTMLElement;
protected showMoreDisplayed = false; protected expanded = false;
protected loadingChangedListener?: CoreEventObserver; protected loadingChangedListener?: CoreEventObserver;
protected emptyText = ''; protected emptyText = '';
protected contentSpan: HTMLElement; protected contentSpan: HTMLElement;
protected toggleExpandEnabled = false;
constructor( constructor(
element: ElementRef, element: ElementRef,
@ -114,11 +115,12 @@ export class CoreFormatTextDirective implements OnChanges {
} }
/** /**
* Detect changes on input properties. * @inheritdoc
*/ */
ngOnChanges(changes: { [name: string]: SimpleChange }): void { ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if (changes.text || changes.filter || changes.contextLevel || changes.contextInstanceId) { if (changes.text || changes.filter || changes.contextLevel || changes.contextInstanceId) {
this.hideShowMore(); this.setExpandButtonEnabled(false);
this.formatAndRenderContents(); this.formatAndRenderContents();
} }
} }
@ -267,37 +269,61 @@ export class CoreFormatTextDirective implements OnChanges {
this.element.style.maxHeight = initialMaxHeight; this.element.style.maxHeight = initialMaxHeight;
// If cannot calculate height, shorten always. // If cannot calculate height, shorten always.
if (!height || height > this.maxHeight) { this.setExpandButtonEnabled(!height || height > this.maxHeight);
if (!this.showMoreDisplayed) {
this.displayShowMore();
}
} else if (this.showMoreDisplayed) {
this.hideShowMore();
}
} }
/** /**
* Display the "Show more" in the element. * Sets if expand button is enabled or not.
*
* @param enable Wether enable or disable.
*/ */
protected displayShowMore(): void { protected setExpandButtonEnabled(enable: boolean): void {
const expandInFullview = CoreUtils.isTrueOrOne(this.fullOnClick) || false; this.toggleExpandEnabled = enable;
const showMoreButton = document.createElement('ion-button'); this.element.classList.toggle('core-text-formatted', enable);
showMoreButton.classList.add('core-show-more'); if (!enable || this.element.querySelector('ion-button.core-format-text-toggle')) {
showMoreButton.setAttribute('fill', 'clear'); return;
showMoreButton.innerHTML = Translate.instant('core.showmore');
this.element.appendChild(showMoreButton);
if (expandInFullview) {
this.element.classList.add('core-expand-in-fullview');
} else {
showMoreButton.setAttribute('aria-expanded', 'false');
} }
this.element.classList.add('core-text-formatted');
this.element.classList.add('core-shortened');
this.element.style.maxHeight = this.maxHeight + 'px';
this.showMoreDisplayed = true; // Add expand/collapse buttons
const toggleButton = document.createElement('ion-button');
toggleButton.classList.add('core-format-text-toggle');
toggleButton.setAttribute('fill', 'clear');
const toggleText = document.createElement('span');
toggleText.classList.add('core-format-text-toggle-text');
toggleButton.appendChild(toggleText);
const expandArrow = document.createElement('span');
expandArrow.classList.add('core-format-text-arrow');
toggleButton.appendChild(expandArrow);
this.element.appendChild(toggleButton);
this.toggleExpand(this.expanded);
}
/**
* Expand or collapse text.
*
* @param expand Wether expand or collapse text. If undefined, will toggle.
*/
protected toggleExpand(expand?: boolean): void {
if (expand === undefined) {
expand = !this.expanded;
}
this.expanded = expand;
this.element.classList.toggle('core-text-format-expanded', expand);
this.element.classList.toggle('core-text-format-collapsed', !expand);
this.element.style.maxHeight = expand ? '' : this.maxHeight + 'px';
const toggleButton = this.element.querySelector('ion-button.core-format-text-toggle');
const toggleText = toggleButton?.querySelector('.core-format-text-toggle-text');
if (!toggleButton || !toggleText) {
return;
}
toggleText.innerHTML = expand ? Translate.instant('core.showless') : Translate.instant('core.showmore');
toggleButton.setAttribute('aria-expanded', expand ? 'true' : 'false');
} }
/** /**
@ -321,9 +347,7 @@ export class CoreFormatTextDirective implements OnChanges {
return; return;
} }
const expandInFullview = CoreUtils.isTrueOrOne(this.fullOnClick) || false; if (!this.toggleExpandEnabled) {
if (!expandInFullview && !this.showMoreDisplayed) {
// Nothing to do on click, just stop. // Nothing to do on click, just stop.
return; return;
} }
@ -331,28 +355,7 @@ export class CoreFormatTextDirective implements OnChanges {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (!expandInFullview) { this.toggleExpand();
// Change class.
this.element.classList.toggle('core-shortened');
return;
} else {
// Open a new state with the contents.
const filter = typeof this.filter != 'undefined' ? CoreUtils.isTrueOrOne(this.filter) : undefined;
CoreTextUtils.viewText(
this.fullTitle || Translate.instant('core.description'),
this.text,
{
component: this.component,
componentId: this.componentId,
filter: filter,
contextLevel: this.contextLevel,
instanceId: this.contextInstanceId,
courseId: this.courseId,
},
);
}
} }
/** /**
@ -361,6 +364,7 @@ export class CoreFormatTextDirective implements OnChanges {
protected finishRender(): void { protected finishRender(): void {
// Show the element again. // Show the element again.
this.element.classList.remove('core-format-text-loading'); this.element.classList.remove('core-format-text-loading');
// Emit the afterRender output. // Emit the afterRender output.
this.afterRender.emit(); this.afterRender.emit();
} }
@ -393,7 +397,7 @@ export class CoreFormatTextDirective implements OnChanges {
this.contentSpan.innerHTML = ''; // Remove current contents. this.contentSpan.innerHTML = ''; // Remove current contents.
if (this.maxHeight && result.div.innerHTML != '' && if (this.maxHeight && result.div.innerHTML != '' &&
(this.fullOnClick || (window.innerWidth < 576 || window.innerHeight < 576))) { // Don't collapse in big screens. (window.innerWidth < 576 || window.innerHeight < 576)) { // Don't collapse in big screens.
// Move the children to the current element to be able to calculate the height. // Move the children to the current element to be able to calculate the height.
CoreDomUtils.moveChildren(result.div, this.contentSpan); CoreDomUtils.moveChildren(result.div, this.contentSpan);
@ -655,20 +659,6 @@ export class CoreFormatTextDirective implements OnChanges {
return CoreDomUtils.getElementHeight(element) || 0; return CoreDomUtils.getElementHeight(element) || 0;
} }
/**
* "Hide" the "Show more" in the element if it's shown.
*/
protected hideShowMore(): void {
const showMoreButton = this.element.querySelector('ion-button.core-show-more');
showMoreButton?.remove();
this.element.classList.remove('core-expand-in-fullview');
this.element.classList.remove('core-text-formatted');
this.element.classList.remove('core-shortened');
this.element.style.maxHeight = '';
this.showMoreDisplayed = false;
}
/** /**
* Add media adapt class and apply CoreExternalContentDirective to the media element and its sources and tracks. * Add media adapt class and apply CoreExternalContentDirective to the media element and its sources and tracks.
* *

View File

@ -1,9 +1,8 @@
<ion-card *ngIf="description"> <ion-card *ngIf="description">
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label> <ion-label>
<core-format-text [text]="description" [component]="component" [componentId]="componentId" <core-format-text [text]="description" [component]="component" [componentId]="componentId" [maxHeight]="120"
[maxHeight]="showFull && showFull !== 'false' ? 0 : 120" fullOnClick="true" [contextLevel]="contextLevel" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
</ion-item> </ion-item>

View File

@ -13,7 +13,7 @@
<ion-item class="ion-text-wrap" *ngIf="description" lines="none"> <ion-item class="ion-text-wrap" *ngIf="description" lines="none">
<ion-label> <ion-label>
<core-format-text [text]="description" [component]="component" [componentId]="componentId" contextLevel="module" <core-format-text [text]="description" [component]="component" [componentId]="componentId" contextLevel="module"
[contextInstanceId]="module.id" [courseId]="courseId" maxHeight="120"> [contextInstanceId]="module.id" [courseId]="courseId" [maxHeight]="120">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
</ion-item> </ion-item>

View File

@ -1,17 +1,12 @@
<ng-container *ngIf="module.handlerData && module.visibleoncoursepage !== 0"> <ng-container *ngIf="module.handlerData && module.visibleoncoursepage !== 0">
<ng-container *ngIf="!module.handlerData.loading"> <ng-container *ngIf="!module.handlerData.loading">
<ion-item <ion-item id="core-course-module-{{module.id}}"
id="core-course-module-{{module.id}}"
class="ion-text-wrap core-course-module-handler core-module-main-item {{module.handlerData.class}}" class="ion-text-wrap core-course-module-handler core-module-main-item {{module.handlerData.class}}"
(click)="moduleClicked($event)" (click)="moduleClicked($event)" [attr.aria-label]="module.handlerData.a11yTitle" [ngClass]="{
[attr.aria-label]="module.handlerData.a11yTitle"
[ngClass]="{
'has-module-info': hasInfo, 'has-module-info': hasInfo,
'item-media': module.handlerData.icon, 'item-media': module.handlerData.icon,
'item-dimmed': module.visible === 0 || module.uservisible === false 'item-dimmed': module.visible === 0 || module.uservisible === false
}" }" [button]="module.handlerData.action && module.uservisible" detail="false">
[button]="module.handlerData.action && module.uservisible"
detail="false">
<core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon" [modname]="module.modname" <core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon" [modname]="module.modname"
[componentId]="module.instance"> [componentId]="module.instance">
@ -23,11 +18,8 @@
[courseId]="courseId" [attr.aria-label]="module.handlerData.a11yTitle + ', ' + modNameTranslated"> [courseId]="courseId" [attr.aria-label]="module.handlerData.a11yTitle + ', ' + modNameTranslated">
</core-format-text> </core-format-text>
</p> </p>
<ion-badge <ion-badge *ngIf="module.handlerData.extraBadge" [color]="module.handlerData.extraBadgeColor"
*ngIf="module.handlerData.extraBadge" class="ion-text-wrap ion-text-start">
[color]="module.handlerData.extraBadgeColor"
class="ion-text-wrap ion-text-start"
>
<span [innerHTML]="module.handlerData.extraBadge"></span> <span [innerHTML]="module.handlerData.extraBadge"></span>
</ion-badge> </ion-badge>
<ion-badge *ngIf="module.visible === 0 && (!section || section.visible)" class="ion-text-wrap"> <ion-badge *ngIf="module.visible === 0 && (!section || section.visible)" class="ion-text-wrap">
@ -47,12 +39,8 @@
</ion-badge> </ion-badge>
</ion-label> </ion-label>
<!-- Buttons. --> <!-- Buttons. -->
<div <div slot="end" *ngIf="module.uservisible !== false" class="buttons core-module-buttons"
slot="end" [ngClass]="{'core-button-completion': module.completiondata && showLegacyCompletion}">
*ngIf="module.uservisible !== false"
class="buttons core-module-buttons"
[ngClass]="{'core-button-completion': module.completiondata && showLegacyCompletion}"
>
<!-- Module completion (legacy). --> <!-- Module completion (legacy). -->
<core-course-module-completion-legacy *ngIf="module.completiondata && showLegacyCompletion" <core-course-module-completion-legacy *ngIf="module.completiondata && showLegacyCompletion"
[completion]="module.completiondata" [moduleName]="module.name" [moduleId]="module.id" [completion]="module.completiondata" [moduleName]="module.name" [moduleId]="module.id"
@ -60,31 +48,24 @@
</core-course-module-completion-legacy> </core-course-module-completion-legacy>
<div class="core-module-buttons-more"> <div class="core-module-buttons-more">
<core-download-refresh [status]="downloadStatus" [enabled]="downloadEnabled" <core-download-refresh [status]="downloadStatus" [enabled]="downloadEnabled" [canTrustDownload]="true"
[canTrustDownload]="true" [loading]="spinner || module.handlerData.spinner" [loading]="spinner || module.handlerData.spinner" (action)="download($event)" size="small">
(action)="download($event)" size="small">
</core-download-refresh> </core-download-refresh>
<!-- Buttons defined by the module handler. --> <!-- Buttons defined by the module handler. -->
<ion-button fill="clear" *ngFor="let button of module.handlerData.buttons" color="dark" size="small" <ion-button fill="clear" *ngFor="let button of module.handlerData.buttons" color="dark" size="small"
[hidden]="button.hidden || spinner || module.handlerData.spinner" class="core-animate-show-hide" [hidden]="button.hidden || spinner || module.handlerData.spinner" class="core-animate-show-hide"
(click)="buttonClicked($event, button)" (click)="buttonClicked($event, button)" [attr.aria-label]="button.label | translate:{$a: module.handlerData.title}">
[attr.aria-label]="button.label | translate:{$a: module.handlerData.title}">
<ion-icon [name]="button.icon" slot="icon-only" aria-hidden="true"></ion-icon> <ion-icon [name]="button.icon" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
</div> </div>
</div> </div>
</ion-item> </ion-item>
<ion-item <ion-item *ngIf="hasInfo" id="core-course-module-{{module.id}}-info"
*ngIf="hasInfo" class="ion-text-wrap core-course-module-handler core-module-module-info {{module.handlerData.class}}" [ngClass]="{
id="core-course-module-{{module.id}}-info"
class="ion-text-wrap core-course-module-handler core-module-module-info {{module.handlerData.class}}"
[ngClass]="{
'item-media': module.handlerData.icon, 'item-media': module.handlerData.icon,
'item-dimmed': module.visible === 0 || module.uservisible === false 'item-dimmed': module.visible === 0 || module.uservisible === false
}" }" detail="false">
detail="false"
>
<ion-label> <ion-label>
<!-- Activity dates. --> <!-- Activity dates. -->
<div *ngIf="showActivityDates && module.dates && module.dates.length" class="core-module-dates"> <div *ngIf="showActivityDates && module.dates && module.dates.length" class="core-module-dates">
@ -94,12 +75,12 @@
</div> </div>
<!-- Module completion. --> <!-- Module completion. -->
<core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata" <core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata" [moduleName]="module.name"
[moduleName]="module.name" [moduleId]="module.id" [showCompletionConditions]="showCompletionConditions" [moduleId]="module.id" [showCompletionConditions]="showCompletionConditions"
[showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)"> [showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
</core-course-module-completion> </core-course-module-completion>
<core-format-text class="core-module-description" *ngIf="module.description" maxHeight="80" [text]="module.description" <core-format-text class="core-module-description" *ngIf="module.description" [maxHeight]="80" [text]="module.description"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
@ -107,14 +88,11 @@
</ng-container> </ng-container>
<!-- Loading. --> <!-- Loading. -->
<ion-item *ngIf="module.handlerData.loading" <ion-item *ngIf="module.handlerData.loading" role="status" class="ion-text-wrap" id="core-course-module-{{module.id}}"
role="status"
class="ion-text-wrap"
id="core-course-module-{{module.id}}"
[attr.aria-label]="module.handlerData.a11yTitle" [attr.aria-label]="module.handlerData.a11yTitle"
[ngClass]="['core-course-module-handler', 'core-module-loading', module.handlerData.class]" [ngClass]="['core-course-module-handler', 'core-module-loading', module.handlerData.class]" detail="false">
detail="false" <ion-label>
> <ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner>
<ion-label><ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner></ion-label> </ion-label>
</ion-item> </ion-item>
</ng-container> </ng-container>

View File

@ -27,8 +27,10 @@
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"> <core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text> </core-format-text>
</h2> </h2>
<p *ngIf="course.categoryname"><core-format-text [text]="course.categoryname" <p *ngIf="course.categoryname">
contextLevel="coursecat" [contextInstanceId]="course.categoryid"></core-format-text></p> <core-format-text [text]="course.categoryname" contextLevel="coursecat" [contextInstanceId]="course.categoryid">
</core-format-text>
</p>
<p *ngIf="course.startdate"> <p *ngIf="course.startdate">
{{course.startdate * 1000 | coreFormatDate:"strftimedatefullshort" }} {{course.startdate * 1000 | coreFormatDate:"strftimedatefullshort" }}
<span *ngIf="course.enddate"> - {{course.enddate * 1000 | coreFormatDate:"strftimedatefullshort" }}</span> <span *ngIf="course.enddate"> - {{course.enddate * 1000 | coreFormatDate:"strftimedatefullshort" }}</span>
@ -38,7 +40,7 @@
<ion-item class="ion-text-wrap" *ngIf="course.summary" detail="false"> <ion-item class="ion-text-wrap" *ngIf="course.summary" detail="false">
<ion-label> <ion-label>
<core-format-text [text]="course.summary" maxHeight="120" contextLevel="course" [contextInstanceId]="course.id"> <core-format-text [text]="course.summary" [maxHeight]="120" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
</ion-item> </ion-item>
@ -51,8 +53,7 @@
</ion-item-divider> </ion-item-divider>
<ion-item class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link [userId]="contact.id" <ion-item class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link [userId]="contact.id"
[courseId]="isEnrolled ? course.id : null" [attr.aria-label]="'core.viewprofile' | translate" detail="true"> [courseId]="isEnrolled ? course.id : null" [attr.aria-label]="'core.viewprofile' | translate" detail="true">
<core-user-avatar [user]="contact" slot="start" [userId]="contact.id" <core-user-avatar [user]="contact" slot="start" [userId]="contact.id" [courseId]="isEnrolled ? course.id : null">
[courseId]="isEnrolled ? course.id : null">
</core-user-avatar> </core-user-avatar>
<ion-label> <ion-label>
<p class="item-heading">{{contact.fullname}}</p> <p class="item-heading">{{contact.fullname}}</p>
@ -71,7 +72,7 @@
</core-format-text> </core-format-text>
</span><span class="core-customfieldseparator">: </span> </span><span class="core-customfieldseparator">: </span>
<span class="core-customfieldvalue"> <span class="core-customfieldvalue">
<core-format-text [text]="field.value" maxHeight="120" contextLevel="course" <core-format-text [text]="field.value" [maxHeight]="120" contextLevel="course"
[contextInstanceId]="course.id"> [contextInstanceId]="course.id">
</core-format-text> </core-format-text>
</span> </span>
@ -100,19 +101,19 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item *ngIf="!isEnrolled && !selfEnrolInstances.length && !paypalEnabled"> <ion-item *ngIf="!isEnrolled && !selfEnrolInstances.length && !paypalEnabled">
<ion-label><p>{{ 'core.courses.notenrollable' | translate }}</p></ion-label> <ion-label>
<p>{{ 'core.courses.notenrollable' | translate }}</p>
</ion-label>
</ion-item> </ion-item>
<ion-item *ngIf="canAccessCourse && downloadCourseEnabled" (click)="prefetchCourse()" detail="false" <ion-item *ngIf="canAccessCourse && downloadCourseEnabled" (click)="prefetchCourse()" detail="false"
[attr.aria-label]="prefetchCourseData.statusTranslatable | translate" button> [attr.aria-label]="prefetchCourseData.statusTranslatable | translate" button>
<ion-icon *ngIf="(prefetchCourseData.status != statusDownloaded) && !prefetchCourseData.loading" <ion-icon *ngIf="(prefetchCourseData.status != statusDownloaded) && !prefetchCourseData.loading"
[name]="prefetchCourseData.icon" slot="start" aria-hidden="true"> [name]="prefetchCourseData.icon" slot="start" aria-hidden="true">
</ion-icon> </ion-icon>
<ion-icon *ngIf="(prefetchCourseData.status == statusDownloaded) && !prefetchCourseData.loading" <ion-icon *ngIf="(prefetchCourseData.status == statusDownloaded) && !prefetchCourseData.loading" slot="start"
slot="start" [name]="prefetchCourseData.icon" color="success" [name]="prefetchCourseData.icon" color="success" aria-hidden="true" role="status">
aria-hidden="true" role="status">
</ion-icon> </ion-icon>
<ion-spinner *ngIf="prefetchCourseData.loading" slot="start" <ion-spinner *ngIf="prefetchCourseData.loading" slot="start" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
[attr.aria-label]="'core.loading' | translate"></ion-spinner>
<ion-label> <ion-label>
<h2 *ngIf="prefetchCourseData.status != statusDownloaded">{{ 'core.course.downloadcourse' | translate }}</h2> <h2 *ngIf="prefetchCourseData.status != statusDownloaded">{{ 'core.course.downloadcourse' | translate }}</h2>
<h2 *ngIf="prefetchCourseData.status == statusDownloaded">{{ 'core.course.refreshcourse' | translate }}</h2> <h2 *ngIf="prefetchCourseData.status == statusDownloaded">{{ 'core.course.refreshcourse' | translate }}</h2>
@ -121,12 +122,15 @@
<ion-item button (click)="openCourse()" [attr.aria-label]="course.fullname" *ngIf="!avoidOpenCourse && canAccessCourse" <ion-item button (click)="openCourse()" [attr.aria-label]="course.fullname" *ngIf="!avoidOpenCourse && canAccessCourse"
detail="true"> detail="true">
<ion-icon name="fas-briefcase" slot="start" aria-hidden="true"></ion-icon> <ion-icon name="fas-briefcase" slot="start" aria-hidden="true"></ion-icon>
<ion-label><h2>{{ 'core.course.contents' | translate }}</h2></ion-label> <ion-label>
<h2>{{ 'core.course.contents' | translate }}</h2>
</ion-label>
</ion-item> </ion-item>
<ion-item [href]="courseUrl" core-link [attr.aria-label]="course.fullname" button detail="false" <ion-item [href]="courseUrl" core-link [attr.aria-label]="course.fullname" button detail="false" [showBrowserWarning]="false">
[showBrowserWarning]="false">
<ion-icon name="fas-external-link-alt" slot="start" aria-hidden="true"></ion-icon> <ion-icon name="fas-external-link-alt" slot="start" aria-hidden="true"></ion-icon>
<ion-label><h2>{{ 'core.openinbrowser' | translate }}</h2></ion-label> <ion-label>
<h2>{{ 'core.openinbrowser' | translate }}</h2>
</ion-label>
</ion-item> </ion-item>
</div> </div>
</core-loading> </core-loading>

View File

@ -31,7 +31,7 @@
</core-format-text> </core-format-text>
</p> </p>
<p *ngIf="currentCategory.description"> <p *ngIf="currentCategory.description">
<core-format-text [text]="currentCategory.description" maxHeight="60" contextLevel="coursecat" <core-format-text [text]="currentCategory.description" [maxHeight]="120" contextLevel="coursecat"
[contextInstanceId]="currentCategory.id"></core-format-text> [contextInstanceId]="currentCategory.id"></core-format-text>
</p> </p>
</ion-label> </ion-label>

View File

@ -251,7 +251,7 @@ export class CoreCoursesHelperProvider {
case 'lastaccess': case 'lastaccess':
courses.sort((a, b) => (b.lastaccess || 0) - (a.lastaccess || 0)); courses.sort((a, b) => (b.lastaccess || 0) - (a.lastaccess || 0));
break; break;
// @todo Time modified property is not defined in CoreEnrolledCourseDataWithOptions, so it won't do nothing. // @todo Time modified property is not defined in CoreEnrolledCourseDataWithOptions, so it Won't do anything.
// case 'timemodified': // case 'timemodified':
// courses.sort((a, b) => b.timemodified - a.timemodified); // courses.sort((a, b) => b.timemodified - a.timemodified);
// break; // break;

View File

@ -14,15 +14,16 @@
<core-empty-box *ngIf="!grade" icon="fas-chart-bar" [message]="'core.grades.nogradesreturned' | translate"></core-empty-box> <core-empty-box *ngIf="!grade" icon="fas-chart-bar" [message]="'core.grades.nogradesreturned' | translate"></core-empty-box>
<ion-list *ngIf="grade"> <ion-list *ngIf="grade">
<ion-item *ngIf="grade.itemname && grade.link" class="ion-text-wrap" detail="true" [href]="grade.link" core-link <ion-item *ngIf="grade.itemname && grade.link" class="ion-text-wrap" detail="true" [href]="grade.link" core-link capture="true">
capture="true">
<ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="start" [attr.aria-label]="grade.iconAlt"></ion-icon> <ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="start" [attr.aria-label]="grade.iconAlt"></ion-icon>
<img *ngIf="grade.image && !grade.itemmodule" [src]="grade.image && grade.itemmodule" slot="start" [alt]="grade.iconAlt" /> <img *ngIf="grade.image && !grade.itemmodule" [src]="grade.image && grade.itemmodule" slot="start" [alt]="grade.iconAlt" />
<core-mod-icon *ngIf="grade.image && grade.itemmodule" [modicon]="grade.image" slot="start" [modname]="grade.itemmodule"> <core-mod-icon *ngIf="grade.image && grade.itemmodule" [modicon]="grade.image" slot="start" [modname]="grade.itemmodule">
</core-mod-icon> </core-mod-icon>
<ion-label> <ion-label>
<h2><core-format-text [text]="grade.itemname" contextLevel="course" [contextInstanceId]="courseId"> <h2>
</core-format-text></h2> <core-format-text [text]="grade.itemname" contextLevel="course" [contextInstanceId]="courseId">
</core-format-text>
</h2>
</ion-label> </ion-label>
</ion-item> </ion-item>
@ -32,8 +33,10 @@
<core-mod-icon *ngIf="grade.image && grade.itemmodule" [modicon]="grade.image" slot="start" [modname]="grade.itemmodule"> <core-mod-icon *ngIf="grade.image && grade.itemmodule" [modicon]="grade.image" slot="start" [modname]="grade.itemmodule">
</core-mod-icon> </core-mod-icon>
<ion-label> <ion-label>
<h2><core-format-text [text]="grade.itemname" contextLevel="course" [contextInstanceId]="courseId"> <h2>
</core-format-text></h2> <core-format-text [text]="grade.itemname" contextLevel="course" [contextInstanceId]="courseId">
</core-format-text>
</h2>
</ion-label> </ion-label>
</ion-item> </ion-item>
@ -89,8 +92,10 @@
<ion-item class="ion-text-wrap" *ngIf="grade.feedback"> <ion-item class="ion-text-wrap" *ngIf="grade.feedback">
<ion-label> <ion-label>
<h2>{{ 'core.grades.feedback' | translate}}</h2> <h2>{{ 'core.grades.feedback' | translate}}</h2>
<p><core-format-text [fullTitle]="'core.grades.feedback' | translate" maxHeight="60" fullOnClick="true" <p>
[text]="grade.feedback" contextLevel="course" [contextInstanceId]="courseId"></core-format-text></p> <core-format-text [maxHeight]="120" [text]="grade.feedback" contextLevel="course" [contextInstanceId]="courseId">
</core-format-text>
</p>
</ion-label> </ion-label>
</ion-item> </ion-item>

View File

@ -571,7 +571,7 @@ export class CoreTextUtilsProvider {
const regex = new RegExp('(' + searchText + ')', 'gi'); const regex = new RegExp('(' + searchText + ')', 'gi');
return text.replace(regex, '<span class="matchtext">$1</span>'); return text.replace(regex, '<mark class="matchtext">$1</mark>');
} }
/** /**

View File

@ -51,11 +51,15 @@ core-format-text {
display: inline; display: inline;
} }
.core-show-more { .core-format-text-toggle {
display: none !important; display: none !important;
} }
} }
.core-format-text-toggle {
display: none;
}
.core-format-text-content { .core-format-text-content {
opacity: 1; opacity: 1;
@include core-transition(opacity, 200ms); @include core-transition(opacity, 200ms);
@ -84,30 +88,45 @@ core-format-text {
cursor: pointer; cursor: pointer;
pointer-events: auto; pointer-events: auto;
.core-show-more { .core-format-text-toggle {
display: none; display: block;
} position: absolute;
bottom: 0;
&:not(.core-shortened) { left: 0;
max-height: none !important; right: 0;
} text-align: center;
z-index: 7;
&.core-shortened {
overflow: hidden;
min-height: 50px;
.core-show-more {
text-transform: none; text-transform: none;
text-align: end; text-align: end;
font-size: 14px; font-size: 14px;
display: block;
position: absolute;
@include position(null, 0, 0, null);
z-index: 7;
background-color: var(--core-format-text-background); background-color: var(--core-format-text-background);
color: var(--text-color); color: var(--text-color);
@include padding(null, null, null, 10px);
margin: 0; margin: 0;
.core-format-text-arrow {
width: var(--a11y-min-target-size);
height: var(--a11y-min-target-size);
background-position: center;
background-repeat: no-repeat;
background-size: 14px 14px;
@include core-transition(transform, 500ms);
@include push-arrow-color(626262, true);
@include darkmode() {
@include push-arrow-color(ffffff, true);
}
}
}
&.core-text-format-collapsed {
overflow: hidden;
min-height: 50px;
.core-format-text-arrow {
transform: rotate(90deg);
} }
&:before { &:before {
@ -115,26 +134,19 @@ core-format-text {
height: 100%; height: 100%;
position: absolute; position: absolute;
@include position(null, 0, 0, 0); @include position(null, 0, 0, 0);
background: -webkit-linear-gradient(top, rgba(var(--core-format-text-background-gradient-rgb), 0) calc(100% - 50px), rgba(var(--core-format-text-background-gradient-rgb), 1) calc(100% - 15px)); background: -webkit-linear-gradient(top, rgba(var(--core-format-text-background-gradient-rgb), 0) calc(100% - 60px), rgba(var(--core-format-text-background-gradient-rgb), 1) calc(100% - 40px));
background: linear-gradient(to bottom, rgba(var(--core-format-text-background-gradient-rgb), 0) calc(100% - 50px), rgba(var(--core-format-text-background-gradient-rgb), 1) calc(100% - 15px)); background: linear-gradient(to bottom, rgba(var(--core-format-text-background-gradient-rgb), 0) calc(100% - 60px), rgba(var(--core-format-text-background-gradient-rgb), 1) calc(100% - 40px));
z-index: 6; z-index: 6;
} }
} }
}
&.core-expand-in-fullview { &.core-text-format-expanded {
cursor: pointer; max-height: none !important;
.core-show-more { padding-bottom: 50px; // So the Show less button can fit.
@include push-arrow-color(626262, true);
@include padding-horizontal(null, 5px);
@include background-position(end, 5px, center);
background-repeat: no-repeat; .core-format-text-arrow {
background-size: 14px 14px; transform: rotate(-90deg);
@include darkmode() {
@include push-arrow-color(ffffff, true);
} }
} }
} }
@ -143,10 +155,10 @@ core-format-text {
@if ($core-format-text-never-shorten) { @if ($core-format-text-never-shorten) {
&[maxHeight], &[maxHeight],
&[ng-reflect-max-height] { &[ng-reflect-max-height] {
&.core-text-formatted.core-shortened { &.core-text-formatted.core-text-format-expanded {
max-height: none !important; max-height: none !important;
.core-show-more { .core-format-text-toggle {
display: none !important; display: none !important;
} }
@ -354,7 +366,7 @@ core-rich-text-editor .core-rte-editor {
height: 30px; height: 30px;
display: inline-block; display: inline-block;
border: 1px solid var(--gray-dark); border: 1px solid var(--gray-dark);
background: var(--background-contrast); background: var(--contrast-background);
padding: 6px 8px; padding: 6px 8px;
border-radius: 4px; border-radius: 4px;
margin-left: 2px; margin-left: 2px;
@ -363,7 +375,7 @@ core-rich-text-editor .core-rte-editor {
} }
select { select {
background-color: var(--background-contrast); background-color: var(--contrast-background);
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23000000%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'); background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23000000%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');
background-repeat: no-repeat, repeat; background-repeat: no-repeat, repeat;
background-position: right .7em top 50%, 0 0; background-position: right .7em top 50%, 0 0;

View File

@ -848,7 +848,7 @@ core-block ion-item-divider .core-button-spinner {
// Text formats. // Text formats.
// Highlight text. // Highlight text.
.matchtext { mark, .matchtext {
background-color: var(--text-hightlight-background-color); background-color: var(--text-hightlight-background-color);
} }