Merge pull request #2281 from dpalou/MOBILE-3323

Mobile 3323
main
Juan Leyva 2020-02-18 14:40:41 +01:00 committed by GitHub
commit 7eec78fb92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 1246 additions and 305 deletions

View File

@ -1482,6 +1482,8 @@
"core.downloaded": "local_moodlemobileapp", "core.downloaded": "local_moodlemobileapp",
"core.downloading": "local_moodlemobileapp", "core.downloading": "local_moodlemobileapp",
"core.edit": "moodle", "core.edit": "moodle",
"core.editor.autosavesucceeded": "editor_atto",
"core.editor.textrecovered": "editor_atto",
"core.emptysplit": "local_moodlemobileapp", "core.emptysplit": "local_moodlemobileapp",
"core.error": "moodle", "core.error": "moodle",
"core.errorchangecompletion": "local_moodlemobileapp", "core.errorchangecompletion": "local_moodlemobileapp",

View File

@ -9,7 +9,7 @@
</ion-refresher> </ion-refresher>
<core-loading [hideUntil]="loaded"> <core-loading [hideUntil]="loaded">
<form ion-list [formGroup]="eventForm" *ngIf="!error"> <form ion-list [formGroup]="eventForm" *ngIf="!error" #editEventForm>
<!-- Event name. --> <!-- Event name. -->
<ion-item text-wrap> <ion-item text-wrap>
<ion-label stacked><h2 [core-mark-required]="true">{{ 'addon.calendar.eventname' | translate }}</h2></ion-label> <ion-label stacked><h2 [core-mark-required]="true">{{ 'addon.calendar.eventname' | translate }}</h2></ion-label>
@ -86,7 +86,7 @@
<!-- Description. --> <!-- Description. -->
<ion-item text-wrap> <ion-item text-wrap>
<ion-label stacked><h2>{{ 'core.description' | translate }}</h2></ion-label> <ion-label stacked><h2>{{ 'core.description' | translate }}</h2></ion-label>
<core-rich-text-editor item-content [control]="descriptionControl" [placeholder]="'core.description' | translate" name="description" [component]="component" [componentId]="eventId"></core-rich-text-editor> <core-rich-text-editor item-content [control]="descriptionControl" [placeholder]="'core.description' | translate" name="description" [component]="component" [componentId]="eventId" [autoSave]="false"></core-rich-text-editor>
</ion-item> </ion-item>
<!-- Location. --> <!-- Location. -->

View File

@ -17,6 +17,7 @@ import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
import { AddonCalendarEditEventPage } from './edit-event'; import { AddonCalendarEditEventPage } from './edit-event';
@NgModule({ @NgModule({
@ -26,6 +27,7 @@ import { AddonCalendarEditEventPage } from './edit-event';
imports: [ imports: [
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule, CoreDirectivesModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonCalendarEditEventPage), IonicPageModule.forChild(AddonCalendarEditEventPage),
TranslateModule.forChild() TranslateModule.forChild()
], ],

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, OnDestroy, Optional, ViewChild } from '@angular/core'; import { Component, OnInit, OnDestroy, Optional, ViewChild, ElementRef } from '@angular/core';
import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms';
import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -25,7 +25,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-text-editor.ts'; import { CoreEditorRichTextEditorComponent } from '@core/editor/components/rich-text-editor/rich-text-editor.ts';
import { AddonCalendarProvider, AddonCalendarGetAccessInfoResult, AddonCalendarEvent } from '../../providers/calendar'; import { AddonCalendarProvider, AddonCalendarGetAccessInfoResult, AddonCalendarEvent } from '../../providers/calendar';
import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarHelperProvider } from '../../providers/helper';
@ -43,7 +43,8 @@ import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
}) })
export class AddonCalendarEditEventPage implements OnInit, OnDestroy { export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
@ViewChild(CoreRichTextEditorComponent) descriptionEditor: CoreRichTextEditorComponent; @ViewChild(CoreEditorRichTextEditorComponent) descriptionEditor: CoreEditorRichTextEditorComponent;
@ViewChild('editEventForm') formElement: ElementRef;
title: string; title: string;
dateFormat: string; dateFormat: string;
@ -496,6 +497,8 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
this.calendarProvider.submitEvent(this.eventId, data).then((result) => { this.calendarProvider.submitEvent(this.eventId, data).then((result) => {
event = result.event; event = result.event;
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, result.sent, this.currentSite.getId());
if (result.sent) { if (result.sent) {
// Event created or edited, invalidate right days & months. // Event created or edited, invalidate right days & months.
const numberOfRepetitions = formData.repeat ? formData.repeats : const numberOfRepetitions = formData.repeat ? formData.repeats :
@ -557,6 +560,9 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
discard(): void { discard(): void {
this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => { this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => {
this.calendarOffline.deleteEvent(this.eventId).then(() => { this.calendarOffline.deleteEvent(this.eventId).then(() => {
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.currentSite.getId());
this.returnToList(); this.returnToList();
}).catch(() => { }).catch(() => {
// Shouldn't happen. // Shouldn't happen.
@ -572,16 +578,18 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
* *
* @return Resolved if we can leave it, rejected if not. * @return Resolved if we can leave it, rejected if not.
*/ */
ionViewCanLeave(): boolean | Promise<void> { async ionViewCanLeave(): Promise<void> {
if (this.calendarHelper.hasEventDataChanged(this.eventForm.value, this.originalData)) { if (this.calendarHelper.hasEventDataChanged(this.eventForm.value, this.originalData)) {
// Show confirmation if some data has been modified. // Show confirmation if some data has been modified.
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
} else {
return Promise.resolve();
}
} }
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.currentSite.getId());
}
/**
* Unblock sync.
*/
protected unblockSync(): void { protected unblockSync(): void {
if (this.eventId) { if (this.eventId) {
this.syncProvider.unblockOperation(AddonCalendarProvider.COMPONENT, this.eventId); this.syncProvider.unblockOperation(AddonCalendarProvider.COMPONENT, this.eventId);

View File

@ -21,6 +21,7 @@ import { AddonModAssignFeedbackCommentsComponent } from './component/comments';
import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegate'; import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegate';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -31,7 +32,8 @@ import { CoreDirectivesModule } from '@directives/directives.module';
IonicModule, IonicModule,
TranslateModule.forChild(), TranslateModule.forChild(),
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule CoreDirectivesModule,
CoreEditorComponentsModule,
], ],
providers: [ providers: [
AddonModAssignFeedbackCommentsHandler AddonModAssignFeedbackCommentsHandler

View File

@ -19,5 +19,6 @@
<!-- Edit --> <!-- Edit -->
<ion-item text-wrap *ngIf="edit && loaded"> <ion-item text-wrap *ngIf="edit && loaded">
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="assignfeedbackcomments_editor" [component]="component" [componentId]="assign.cmid"></core-rich-text-editor> <core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="assignfeedbackcomments_editor" [component]="component" [componentId]="assign.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="assign.cmid" elementId="assignfeedbackcomments_editor" [draftExtraParams]="{userid: userId, action: 'grade'}">
</core-rich-text-editor>
</ion-item> </ion-item>

View File

@ -9,7 +9,7 @@
</ion-navbar> </ion-navbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<form name="addon-mod_assign-edit-feedback-form" *ngIf="userId && plugin"> <form name="addon-mod_assign-edit-feedback-form" *ngIf="userId && plugin" #editFeedbackForm>
<addon-mod-assign-feedback-plugin [assign]="assign" [submission]="submission" [userId]="userId" [plugin]="plugin" [edit]="true"></addon-mod-assign-feedback-plugin> <addon-mod-assign-feedback-plugin [assign]="assign" [submission]="submission" [userId]="userId" [plugin]="plugin" [edit]="true"></addon-mod-assign-feedback-plugin>
<button ion-button block (click)="done($event)">{{ 'core.done' | translate }}</button> <button ion-button block (click)="done($event)">{{ 'core.done' | translate }}</button>
</form> </form>

View File

@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, Input } from '@angular/core'; import { Component, Input, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, ViewController, NavParams } from 'ionic-angular'; import { IonicPage, ViewController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegate'; import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegate';
import { import {
@ -36,10 +38,17 @@ export class AddonModAssignEditFeedbackModalPage {
@Input() plugin: AddonModAssignPlugin; // The plugin object. @Input() plugin: AddonModAssignPlugin; // The plugin object.
@Input() userId: number; // The user ID of the submission. @Input() userId: number; // The user ID of the submission.
@ViewChild('editFeedbackForm') formElement: ElementRef;
protected forceLeave = false; // To allow leaving the page without checking for changes. protected forceLeave = false; // To allow leaving the page without checking for changes.
constructor(params: NavParams, protected viewCtrl: ViewController, protected domUtils: CoreDomUtilsProvider, constructor(params: NavParams,
protected translate: TranslateService, protected feedbackDelegate: AddonModAssignFeedbackDelegate) { protected viewCtrl: ViewController,
protected domUtils: CoreDomUtilsProvider,
protected translate: TranslateService,
protected feedbackDelegate: AddonModAssignFeedbackDelegate,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider) {
this.assign = params.get('assign'); this.assign = params.get('assign');
this.submission = params.get('submission'); this.submission = params.get('submission');
@ -52,16 +61,17 @@ export class AddonModAssignEditFeedbackModalPage {
* *
* @return Resolved if we can leave it, rejected if not. * @return Resolved if we can leave it, rejected if not.
*/ */
ionViewCanLeave(): boolean | Promise<void> { async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) { if (this.forceLeave) {
return true; return;
} }
return this.hasDataChanged().then((changed) => { const changed = await this.hasDataChanged();
if (changed) { if (changed) {
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
} }
});
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
} }
/** /**
@ -82,6 +92,8 @@ export class AddonModAssignEditFeedbackModalPage {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false, this.sitesProvider.getCurrentSiteId());
// Close the modal, sending the input data. // Close the modal, sending the input data.
this.forceLeave = true; this.forceLeave = true;
this.closeModal(this.getInputData()); this.closeModal(this.getInputData());

View File

@ -13,7 +13,7 @@
<core-loading [hideUntil]="loaded"> <core-loading [hideUntil]="loaded">
<ion-list> <ion-list>
<!-- @todo: plagiarism_print_disclosure --> <!-- @todo: plagiarism_print_disclosure -->
<form name="addon-mod_assign-edit-form" *ngIf="userSubmission && userSubmission.plugins && userSubmission.plugins.length"> <form name="addon-mod_assign-edit-form" *ngIf="userSubmission && userSubmission.plugins && userSubmission.plugins.length" #editSubmissionForm>
<!-- Submission statement. --> <!-- Submission statement. -->
<ion-item text-wrap *ngIf="submissionStatement"> <ion-item text-wrap *ngIf="submissionStatement">
<ion-label><core-format-text [text]="submissionStatement" [filter]="false"></core-format-text></ion-label> <ion-label><core-format-text [text]="submissionStatement" [filter]="false"></core-format-text></ion-label>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
@ -34,6 +34,9 @@ import { AddonModAssignHelperProvider } from '../../providers/helper';
templateUrl: 'edit.html', templateUrl: 'edit.html',
}) })
export class AddonModAssignEditPage implements OnInit, OnDestroy { export class AddonModAssignEditPage implements OnInit, OnDestroy {
@ViewChild('editSubmissionForm') formElement: ElementRef;
title: string; // Title to display. title: string; // Title to display.
assign: AddonModAssignAssign; // Assignment. assign: AddonModAssignAssign; // Assignment.
courseId: number; // Course ID the assignment belongs to. courseId: number; // Course ID the assignment belongs to.
@ -82,20 +85,21 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy {
* *
* @return Resolved if we can leave it, rejected if not. * @return Resolved if we can leave it, rejected if not.
*/ */
ionViewCanLeave(): boolean | Promise<void> { async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) { if (this.forceLeave) {
return true; return;
} }
// Check if data has changed. // Check if data has changed.
return this.hasDataChanged().then((changed) => { const changed = await this.hasDataChanged();
if (changed) { if (changed) {
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
} }
}).then(() => {
// Nothing has changed or user confirmed to leave. Clear temporary data from plugins. // Nothing has changed or user confirmed to leave. Clear temporary data from plugins.
this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, this.getInputData()); this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, this.getInputData());
});
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
} }
/** /**
@ -265,50 +269,57 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy {
* *
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected saveSubmission(): Promise<any> { protected async saveSubmission(): Promise<void> {
const inputData = this.getInputData(); const inputData = this.getInputData();
if (this.submissionStatement && (!inputData.submissionstatement || inputData.submissionstatement === 'false')) { if (this.submissionStatement && (!inputData.submissionstatement || inputData.submissionstatement === 'false')) {
return Promise.reject(this.translate.instant('addon.mod_assign.acceptsubmissionstatement')); throw this.translate.instant('addon.mod_assign.acceptsubmissionstatement');
} }
let modal = this.domUtils.showModalLoading(); let modal = this.domUtils.showModalLoading();
let size;
// Get size to ask for confirmation. // Get size to ask for confirmation.
return this.assignHelper.getSubmissionSizeForEdit(this.assign, this.userSubmission, inputData).catch(() => { try {
size = await this.assignHelper.getSubmissionSizeForEdit(this.assign, this.userSubmission, inputData);
} catch (error) {
// Error calculating size, return -1. // Error calculating size, return -1.
return -1; size = -1;
}).then((size) => { }
modal.dismiss(); modal.dismiss();
try {
// Confirm action. // Confirm action.
return this.fileUploaderHelper.confirmUploadFile(size, true, this.allowOffline); await this.fileUploaderHelper.confirmUploadFile(size, true, this.allowOffline);
}).then(() => {
modal = this.domUtils.showModalLoading('core.sending', true); modal = this.domUtils.showModalLoading('core.sending', true);
return this.prepareSubmissionData(inputData).then((pluginData) => { const pluginData = await this.prepareSubmissionData(inputData);
if (!Object.keys(pluginData).length) { if (!Object.keys(pluginData).length) {
// Nothing to save. // Nothing to save.
return; return;
} }
let promise; let sent: boolean;
if (this.saveOffline) { if (this.saveOffline) {
// Save submission in offline. // Save submission in offline.
promise = this.assignOfflineProvider.saveSubmission(this.assign.id, this.courseId, pluginData, sent = false;
await this.assignOfflineProvider.saveSubmission(this.assign.id, this.courseId, pluginData,
this.userSubmission.timemodified, !this.assign.submissiondrafts, this.userId); this.userSubmission.timemodified, !this.assign.submissiondrafts, this.userId);
} else { } else {
// Try to send it to server. // Try to send it to server.
promise = this.assignProvider.saveSubmission(this.assign.id, this.courseId, pluginData, this.allowOffline, sent = await this.assignProvider.saveSubmission(this.assign.id, this.courseId, pluginData, this.allowOffline,
this.userSubmission.timemodified, !!this.assign.submissiondrafts, this.userId); this.userSubmission.timemodified, !!this.assign.submissiondrafts, this.userId);
} }
return promise.then(() => {
// Clear temporary data from plugins. // Clear temporary data from plugins.
return this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, inputData); await this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, inputData);
}).then(() => {
// Submission saved, trigger event. // Submission saved, trigger events.
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, sent, this.sitesProvider.getCurrentSiteId());
const params = { const params = {
assignmentId: this.assign.id, assignmentId: this.assign.id,
submissionId: this.userSubmission.id, submissionId: this.userSubmission.id,
@ -323,11 +334,9 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy {
this.eventsProvider.trigger(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, params, this.eventsProvider.trigger(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, params,
this.sitesProvider.getCurrentSiteId()); this.sitesProvider.getCurrentSiteId());
} }
}); } finally {
});
}).finally(() => {
modal.dismiss(); modal.dismiss();
}); }
} }
/** /**

View File

@ -15,6 +15,6 @@
<p>{{ 'core.numwords' | translate: {'$a': words + ' / ' + configs.wordlimit} }}</p> <p>{{ 'core.numwords' | translate: {'$a': words + ' / ' + configs.wordlimit} }}</p>
</ion-item> </ion-item>
<ion-item text-wrap> <ion-item text-wrap>
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="onlinetext_editor_text" (contentChanged)="onChange($event)" [component]="component" [componentId]="assign.cmid"></core-rich-text-editor> <core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="onlinetext_editor_text" (contentChanged)="onChange($event)" [component]="component" [componentId]="assign.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="assign.cmid" elementId="onlinetext_editor" [draftExtraParams]="{userid: currentUserId, action: 'editsubmission'}"></core-rich-text-editor>
</ion-item> </ion-item>
</div> </div>

View File

@ -14,6 +14,7 @@
import { Component, OnInit, ElementRef } from '@angular/core'; import { Component, OnInit, ElementRef } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms'; import { FormBuilder, FormControl } from '@angular/forms';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { AddonModAssignProvider } from '../../../providers/assign'; import { AddonModAssignProvider } from '../../../providers/assign';
@ -35,16 +36,23 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS
text: string; text: string;
loaded: boolean; loaded: boolean;
wordLimitEnabled: boolean; wordLimitEnabled: boolean;
currentUserId: number;
protected wordCountTimeout: any; protected wordCountTimeout: any;
protected element: HTMLElement; protected element: HTMLElement;
constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, constructor(
protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider, protected fb: FormBuilder,
element: ElementRef) { protected domUtils: CoreDomUtilsProvider,
protected textUtils: CoreTextUtilsProvider,
protected assignProvider: AddonModAssignProvider,
protected assignOfflineProvider: AddonModAssignOfflineProvider,
element: ElementRef,
sitesProvider: CoreSitesProvider) {
super(); super();
this.element = element.nativeElement; this.element = element.nativeElement;
this.currentUserId = sitesProvider.getCurrentSiteUserId();
} }
/** /**

View File

@ -21,6 +21,7 @@ import { AddonModAssignSubmissionOnlineTextComponent } from './component/onlinet
import { AddonModAssignSubmissionDelegate } from '../../providers/submission-delegate'; import { AddonModAssignSubmissionDelegate } from '../../providers/submission-delegate';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -31,7 +32,8 @@ import { CoreDirectivesModule } from '@directives/directives.module';
IonicModule, IonicModule,
TranslateModule.forChild(), TranslateModule.forChild(),
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule CoreDirectivesModule,
CoreEditorComponentsModule,
], ],
providers: [ providers: [
AddonModAssignSubmissionOnlineTextHandler AddonModAssignSubmissionOnlineTextHandler

View File

@ -40,7 +40,7 @@ import { AddonModDataFieldUrlModule } from './url/url.module';
AddonModDataFieldRadiobuttonModule, AddonModDataFieldRadiobuttonModule,
AddonModDataFieldTextModule, AddonModDataFieldTextModule,
AddonModDataFieldTextareaModule, AddonModDataFieldTextareaModule,
AddonModDataFieldUrlModule AddonModDataFieldUrlModule,
], ],
providers: [ providers: [
], ],

View File

@ -2,7 +2,7 @@
<ion-input *ngIf="mode == 'search'" type="text" [placeholder]="field.name" [formControlName]="'f_'+field.id"></ion-input> <ion-input *ngIf="mode == 'search'" type="text" [placeholder]="field.name" [formControlName]="'f_'+field.id"></ion-input>
<span *ngIf="mode == 'edit'" [core-mark-required]="field.required" class="core-mark-required"></span> <span *ngIf="mode == 'edit'" [core-mark-required]="field.required" class="core-mark-required"></span>
<core-rich-text-editor *ngIf="mode == 'edit'" item-content [control]="form.controls['f_'+field.id]" [placeholder]="field.name" [formControlName]="'f_'+field.id" [component]="component" [componentId]="componentId"></core-rich-text-editor> <core-rich-text-editor *ngIf="mode == 'edit'" item-content [control]="form.controls['f_'+field.id]" [placeholder]="field.name" [formControlName]="'f_'+field.id" [component]="component" [componentId]="componentId" [autoSave]="true" contextLevel="module" [contextInstanceId]="componentId" [elementId]="'field_'+field.id"></core-rich-text-editor>
<core-input-errors *ngIf="error && mode == 'edit'" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors> <core-input-errors *ngIf="error && mode == 'edit'" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span> </span>

View File

@ -49,10 +49,10 @@ export class AddonModDataFieldTextareaComponent extends AddonModDataFieldPluginC
* Initialize field. * Initialize field.
*/ */
protected init(): void { protected init(): void {
if (this.isShowOrListMode()) {
this.component = AddonModDataProvider.COMPONENT; this.component = AddonModDataProvider.COMPONENT;
this.componentId = this.database.coursemodule; this.componentId = this.database.coursemodule;
if (this.isShowOrListMode()) {
return; return;
} }

View File

@ -20,6 +20,7 @@ import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataFieldTextareaComponent } from './component/textarea'; import { AddonModDataFieldTextareaComponent } from './component/textarea';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -30,7 +31,8 @@ import { CoreDirectivesModule } from '@directives/directives.module';
IonicModule, IonicModule,
TranslateModule.forChild(), TranslateModule.forChild(),
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule CoreDirectivesModule,
CoreEditorComponentsModule,
], ],
providers: [ providers: [
AddonModDataFieldTextareaHandler AddonModDataFieldTextareaHandler

View File

@ -21,7 +21,7 @@
<div class="addon-data-contents addon-data-entries-{{data.id}}" *ngIf="data"> <div class="addon-data-contents addon-data-entries-{{data.id}}" *ngIf="data">
<core-style [css]="data.csstemplate" prefix=".addon-data-entries-{{data.id}}"></core-style> <core-style [css]="data.csstemplate" prefix=".addon-data-entries-{{data.id}}"></core-style>
<form (ngSubmit)="save($event)" [formGroup]="editForm"> <form (ngSubmit)="save($event)" [formGroup]="editForm" #editFormEl>
<core-compile-html [text]="editFormRender" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html> <core-compile-html [text]="editFormRender" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html>
</form> </form>
</div> </div>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, ViewChild } from '@angular/core'; import { Component, ViewChild, ElementRef } from '@angular/core';
import { Content, IonicPage, NavParams, NavController } from 'ionic-angular'; import { Content, IonicPage, NavParams, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
@ -40,6 +40,7 @@ import { CoreTagProvider } from '@core/tag/providers/tag';
}) })
export class AddonModDataEditPage { export class AddonModDataEditPage {
@ViewChild(Content) content: Content; @ViewChild(Content) content: Content;
@ViewChild('editFormEl') formElement: ElementRef;
protected module: any; protected module: any;
protected courseId: number; protected courseId: number;
@ -95,28 +96,25 @@ export class AddonModDataEditPage {
* *
* @return Resolved if we can leave it, rejected if not. * @return Resolved if we can leave it, rejected if not.
*/ */
ionViewCanLeave(): boolean | Promise<void> { async ionViewCanLeave(): Promise<void> {
if (this.forceLeave || !this.entry) { if (this.forceLeave || !this.entry) {
return true; return;
} }
const inputData = this.editForm.value; const inputData = this.editForm.value;
return this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, const changed = await this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, this.entry.contents);
this.entry.contents).then((changed) => {
if (!changed) { if (changed) {
return Promise.resolve(); // Show confirmation if some data has been modified.
await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
} }
// Show confirmation if some data has been modified.
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}).then(() => {
// Delete the local files from the tmp folder. // Delete the local files from the tmp folder.
return this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id, const files = await this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id, this.entry.contents);
this.entry.contents).then((files) => {
this.fileUploaderProvider.clearTmpFiles(files); this.fileUploaderProvider.clearTmpFiles(files);
});
}); this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.siteId);
} }
/** /**
@ -216,6 +214,9 @@ export class AddonModDataEditPage {
// This is done if entry is updated when editing or creating if not. // This is done if entry is updated when editing or creating if not.
if ((this.entryId && result.updated) || (!this.entryId && result.newentryid)) { if ((this.entryId && result.updated) || (!this.entryId && result.newentryid)) {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, result.sent, this.siteId);
const promises = []; const promises = [];
this.entryId = this.entryId || result.newentryid; this.entryId = this.entryId || result.newentryid;

View File

@ -13,7 +13,7 @@
<a class="tab-slide" [attr.aria-selected]="!search.searchingAdvanced" (click)="changeAdvanced(false)">{{ 'addon.mod_data.search' | translate}}</a> <a class="tab-slide" [attr.aria-selected]="!search.searchingAdvanced" (click)="changeAdvanced(false)">{{ 'addon.mod_data.search' | translate}}</a>
<a class="tab-slide" [attr.aria-selected]="search.searchingAdvanced" (click)="changeAdvanced(true)">{{ 'addon.mod_data.advancedsearch' | translate }}</a> <a class="tab-slide" [attr.aria-selected]="search.searchingAdvanced" (click)="changeAdvanced(true)">{{ 'addon.mod_data.advancedsearch' | translate }}</a>
</div> </div>
<form (ngSubmit)="searchEntries($event)" [formGroup]="searchForm"> <form (ngSubmit)="searchEntries($event)" [formGroup]="searchForm" #searchFormEl>
<ion-list no-margin> <ion-list no-margin>
<ion-item [hidden]="search.searchingAdvanced"> <ion-item [hidden]="search.searchingAdvanced">
<ion-input type="text" placeholder="{{ 'addon.mod_data.search' | translate}}" [(ngModel)]="search.text" name="text" formControlName="text"></ion-input> <ion-input type="text" placeholder="{{ 'addon.mod_data.search' | translate}}" [(ngModel)]="search.text" name="text" formControlName="text"></ion-input>

View File

@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component } from '@angular/core'; import { Component, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavParams, ViewController } from 'ionic-angular'; import { IonicPage, NavParams, ViewController } from 'ionic-angular';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
@ -32,6 +34,8 @@ import { CoreTagProvider } from '@core/tag/providers/tag';
templateUrl: 'search.html', templateUrl: 'search.html',
}) })
export class AddonModDataSearchPage { export class AddonModDataSearchPage {
@ViewChild('searchFormEl') formElement: ElementRef;
search: any; search: any;
fields: any; fields: any;
data: any; data: any;
@ -41,10 +45,17 @@ export class AddonModDataSearchPage {
jsData: any; jsData: any;
fieldsArray: any; fieldsArray: any;
constructor(params: NavParams, private viewCtrl: ViewController, fb: FormBuilder, protected utils: CoreUtilsProvider, constructor(params: NavParams,
protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate, protected viewCtrl: ViewController,
protected textUtils: CoreTextUtilsProvider, protected dataHelper: AddonModDataHelperProvider, fb: FormBuilder,
private tagProvider: CoreTagProvider) { protected utils: CoreUtilsProvider,
protected domUtils: CoreDomUtilsProvider,
protected fieldsDelegate: AddonModDataFieldsDelegate,
protected textUtils: CoreTextUtilsProvider,
protected dataHelper: AddonModDataHelperProvider,
protected tagProvider: CoreTagProvider,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider) {
this.search = params.get('search'); this.search = params.get('search');
this.fields = params.get('fields'); this.fields = params.get('fields');
this.data = params.get('data'); this.data = params.get('data');
@ -175,6 +186,12 @@ export class AddonModDataSearchPage {
* @param data Data to return to the page. * @param data Data to return to the page.
*/ */
closeModal(data?: any): void { closeModal(data?: any): void {
if (typeof data == 'undefined') {
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
} else {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false, this.sitesProvider.getCurrentSiteId());
}
this.viewCtrl.dismiss(data); this.viewCtrl.dismiss(data);
} }

View File

@ -126,7 +126,8 @@ export class AddonModDataProvider {
.then((entry) => { .then((entry) => {
return { return {
// Return provissional entry Id. // Return provissional entry Id.
newentryid: entry newentryid: entry,
sent: false,
}; };
}); });
}; };
@ -142,7 +143,11 @@ export class AddonModDataProvider {
return storeOffline(); return storeOffline();
} }
return this.addEntryOnline(dataId, contents, groupId, siteId).catch((error) => { return this.addEntryOnline(dataId, contents, groupId, siteId).then((result) => {
result.sent = true;
return result;
}).catch((error) => {
if (this.utils.isWebServiceError(error)) { if (this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted. // The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error); return Promise.reject(error);
@ -194,7 +199,12 @@ export class AddonModDataProvider {
const storeOffline = (): Promise<any> => { const storeOffline = (): Promise<any> => {
const action = approve ? 'approve' : 'disapprove'; const action = approve ? 'approve' : 'disapprove';
return this.dataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId); return this.dataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId)
.then(() => {
return {
sent: false,
};
});
}; };
// Get if the opposite action is not synced. // Get if the opposite action is not synced.
@ -210,7 +220,11 @@ export class AddonModDataProvider {
return storeOffline(); return storeOffline();
} }
return this.approveEntryOnline(entryId, approve, siteId).catch((error) => { return this.approveEntryOnline(entryId, approve, siteId).then(() => {
return {
sent: true,
};
}).catch((error) => {
if (this.utils.isWebServiceError(error)) { if (this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted. // The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error); return Promise.reject(error);
@ -288,7 +302,12 @@ export class AddonModDataProvider {
// Convenience function to store a data to be synchronized later. // Convenience function to store a data to be synchronized later.
const storeOffline = (): Promise<any> => { const storeOffline = (): Promise<any> => {
return this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId); return this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId)
.then(() => {
return {
sent: false,
};
});
}; };
let justAdded = false; let justAdded = false;
@ -318,7 +337,11 @@ export class AddonModDataProvider {
return storeOffline(); return storeOffline();
} }
return this.deleteEntryOnline(entryId, siteId).catch((error) => { return this.deleteEntryOnline(entryId, siteId).then(() => {
return {
sent: true,
};
}).catch((error) => {
if (this.utils.isWebServiceError(error)) { if (this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted. // The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error); return Promise.reject(error);
@ -368,7 +391,8 @@ export class AddonModDataProvider {
return this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId) return this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId)
.then(() => { .then(() => {
return { return {
updated: true updated: true,
sent: false,
}; };
}); });
}; };
@ -408,6 +432,7 @@ export class AddonModDataProvider {
return this.addEntry(dataId, entryId, courseId, contents, groupId, fields, siteId, forceOffline) return this.addEntry(dataId, entryId, courseId, contents, groupId, fields, siteId, forceOffline)
.then((result) => { .then((result) => {
result.updated = true; result.updated = true;
result.sent = true;
return result; return result;
}); });
@ -418,7 +443,11 @@ export class AddonModDataProvider {
return storeOffline(); return storeOffline();
} }
return this.editEntryOnline(entryId, contents, siteId).catch((error) => { return this.editEntryOnline(entryId, contents, siteId).then((result) => {
result.sent = true;
return result;
}).catch((error) => {
if (this.utils.isWebServiceError(error)) { if (this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted. // The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error); return Promise.reject(error);

View File

@ -26,6 +26,7 @@ import { AddonModForumIndexComponent } from './index/index';
import { AddonModForumPostComponent } from './post/post'; import { AddonModForumPostComponent } from './post/post';
import { AddonForumDiscussionOptionsMenuComponent } from './discussion-options-menu/discussion-options-menu'; import { AddonForumDiscussionOptionsMenuComponent } from './discussion-options-menu/discussion-options-menu';
import { AddonForumPostOptionsMenuComponent } from './post-options-menu/post-options-menu'; import { AddonForumPostOptionsMenuComponent } from './post-options-menu/post-options-menu';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -43,7 +44,8 @@ import { AddonForumPostOptionsMenuComponent } from './post-options-menu/post-opt
CorePipesModule, CorePipesModule,
CoreCourseComponentsModule, CoreCourseComponentsModule,
CoreRatingComponentsModule, CoreRatingComponentsModule,
CoreTagComponentsModule CoreTagComponentsModule,
CoreEditorComponentsModule,
], ],
providers: [ providers: [
], ],

View File

@ -64,7 +64,7 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label> <ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.replyplaceholder' | translate" [name]="'mod_forum_reply_' + post.id" [component]="component" [componentId]="componentId"></core-rich-text-editor> <core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.replyplaceholder' | translate" [name]="'mod_forum_reply_' + post.id" [component]="component" [componentId]="componentId" [autoSave]="true" contextLevel="module" [contextInstanceId]="forum && forum.cmid" elementId="message" [draftExtraParams]="{reply: post.id}"></core-rich-text-editor>
</ion-item> </ion-item>
<ion-item text-wrap *ngIf="accessInfo.canpostprivatereply"> <ion-item text-wrap *ngIf="accessInfo.canpostprivatereply">
<ion-label>{{ 'addon.mod_forum.privatereply' | translate }}</ion-label> <ion-label>{{ 'addon.mod_forum.privatereply' | translate }}</ion-label>

View File

@ -16,7 +16,7 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label> <ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.replyplaceholder' | translate" [name]="'mod_forum_reply_' + replyData.id" [component]="component" [componentId]="componentId"></core-rich-text-editor> <core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.replyplaceholder' | translate" [name]="'mod_forum_reply_' + replyData.id" [component]="component" [componentId]="componentId" [autoSave]="true" contextLevel="module" [contextInstanceId]="forum.cmid" elementId="message" [draftExtraParams]="{edit: replyData.id}"></core-rich-text-editor>
</ion-item> </ion-item>
<ion-item-divider text-wrap (click)="toggleAdvanced()" class="core-expandable"> <ion-item-divider text-wrap (click)="toggleAdvanced()" class="core-expandable">
<core-icon *ngIf="!advanced" name="fa-caret-right" item-start></core-icon> <core-icon *ngIf="!advanced" name="fa-caret-right" item-start></core-icon>

View File

@ -19,6 +19,7 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModForumComponentsModule } from '../../components/components.module'; import { AddonModForumComponentsModule } from '../../components/components.module';
import { AddonModForumEditPostPage } from './edit-post'; import { AddonModForumEditPostPage } from './edit-post';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -28,6 +29,7 @@ import { AddonModForumEditPostPage } from './edit-post';
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule, CoreDirectivesModule,
AddonModForumComponentsModule, AddonModForumComponentsModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModForumEditPostPage), IonicPageModule.forChild(AddonModForumEditPostPage),
TranslateModule.forChild() TranslateModule.forChild()
], ],

View File

@ -19,7 +19,7 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label> <ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.message' | translate" name="addon_mod_forum_new_discussion" [component]="component" [componentId]="forum.cmid"></core-rich-text-editor> <core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.message' | translate" name="addon_mod_forum_new_discussion" [component]="component" [componentId]="forum.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="forum.cmid" elementId="message"></core-rich-text-editor>
</ion-item> </ion-item>
<ion-item-divider text-wrap (click)="toggleAdvanced()" class="core-expandable"> <ion-item-divider text-wrap (click)="toggleAdvanced()" class="core-expandable">
<core-icon *ngIf="!advanced" name="fa-caret-right" item-start></core-icon> <core-icon *ngIf="!advanced" name="fa-caret-right" item-start></core-icon>

View File

@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModForumNewDiscussionPage } from './new-discussion'; import { AddonModForumNewDiscussionPage } from './new-discussion';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -26,6 +27,7 @@ import { AddonModForumNewDiscussionPage } from './new-discussion';
imports: [ imports: [
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule, CoreDirectivesModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModForumNewDiscussionPage), IonicPageModule.forChild(AddonModForumNewDiscussionPage),
TranslateModule.forChild() TranslateModule.forChild()
], ],

View File

@ -25,7 +25,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-text-editor.ts'; import { CoreEditorRichTextEditorComponent } from '@core/editor/components/rich-text-editor/rich-text-editor.ts';
import { AddonModForumProvider } from '../../providers/forum'; import { AddonModForumProvider } from '../../providers/forum';
import { AddonModForumOfflineProvider } from '../../providers/offline'; import { AddonModForumOfflineProvider } from '../../providers/offline';
import { AddonModForumHelperProvider } from '../../providers/helper'; import { AddonModForumHelperProvider } from '../../providers/helper';
@ -41,7 +41,7 @@ import { AddonModForumSyncProvider } from '../../providers/sync';
}) })
export class AddonModForumNewDiscussionPage implements OnDestroy { export class AddonModForumNewDiscussionPage implements OnDestroy {
@ViewChild(CoreRichTextEditorComponent) messageEditor: CoreRichTextEditorComponent; @ViewChild(CoreEditorRichTextEditorComponent) messageEditor: CoreEditorRichTextEditorComponent;
component = AddonModForumProvider.COMPONENT; component = AddonModForumProvider.COMPONENT;
messageControl = new FormControl(); messageControl = new FormControl();

View File

@ -15,7 +15,7 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-label stacked>{{ 'addon.mod_glossary.definition' | translate }}</ion-label> <ion-label stacked>{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="definitionControl" (contentChanged)="onDefinitionChange($event)" [placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit" [component]="component" [componentId]="glossary.cmid"></core-rich-text-editor> <core-rich-text-editor item-content [control]="definitionControl" (contentChanged)="onDefinitionChange($event)" [placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit" [component]="component" [componentId]="glossary.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="module.id" elementId="definition_editor" [draftExtraParams]="editorExtraParams"></core-rich-text-editor>
</ion-item> </ion-item>
<ion-item *ngIf="categories.length > 0"> <ion-item *ngIf="categories.length > 0">
<ion-label stacked id="addon-mod-glossary-categories-label">{{ 'addon.mod_glossary.categories' | translate }}</ion-label> <ion-label stacked id="addon-mod-glossary-categories-label">{{ 'addon.mod_glossary.categories' | translate }}</ion-label>

View File

@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModGlossaryEditPage } from './edit'; import { AddonModGlossaryEditPage } from './edit';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -26,6 +27,7 @@ import { AddonModGlossaryEditPage } from './edit';
imports: [ imports: [
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule, CoreDirectivesModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModGlossaryEditPage), IonicPageModule.forChild(AddonModGlossaryEditPage),
TranslateModule.forChild() TranslateModule.forChild()
], ],

View File

@ -51,6 +51,7 @@ export class AddonModGlossaryEditPage implements OnInit {
attachments = []; attachments = [];
definitionControl = new FormControl(); definitionControl = new FormControl();
categories = []; categories = [];
editorExtraParams: {[name: string]: any} = {};
protected courseId: number; protected courseId: number;
protected module: any; protected module: any;
@ -113,6 +114,10 @@ export class AddonModGlossaryEditPage implements OnInit {
this.originalData.files = files.slice(); this.originalData.files = files.slice();
}); });
} }
if (entry.id) {
this.editorExtraParams.id = entry.id;
}
} }
this.definitionControl.setValue(this.entry.definition); this.definitionControl.setValue(this.entry.definition);

View File

@ -34,7 +34,7 @@
<!-- Input password for protected lessons. --> <!-- Input password for protected lessons. -->
<ion-card *ngIf="askPassword"> <ion-card *ngIf="askPassword">
<form ion-list (ngSubmit)="submitPassword($event, passwordinput)"> <form ion-list (ngSubmit)="submitPassword($event, passwordinput)" #passwordForm>
<ion-item text-wrap> <ion-item text-wrap>
<ion-label stacked>{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label> <ion-label stacked>{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label>
<core-show-password item-content [name]="'password'"> <core-show-password item-content [name]="'password'">

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, Optional, Injector, Input, ViewChild } from '@angular/core'; import { Component, Optional, Injector, Input, ViewChild, ElementRef } from '@angular/core';
import { Content, NavController } from 'ionic-angular'; import { Content, NavController } from 'ionic-angular';
import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups';
import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreTimeUtilsProvider } from '@providers/utils/time';
@ -35,6 +35,7 @@ import { CoreTabsComponent } from '@components/tabs/tabs';
}) })
export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityComponent { export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityComponent {
@ViewChild(CoreTabsComponent) tabsComponent: CoreTabsComponent; @ViewChild(CoreTabsComponent) tabsComponent: CoreTabsComponent;
@ViewChild('passwordForm') formElement: ElementRef;
@Input() group: number; // The group to display. @Input() group: number; // The group to display.
@Input() action: string; // The "action" to display first. @Input() action: string; // The "action" to display first.
@ -584,6 +585,8 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
this.loaded = true; this.loaded = true;
this.refreshIcon = 'refresh'; this.refreshIcon = 'refresh';
this.syncIcon = 'sync'; this.syncIcon = 'sync';
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, true, this.siteId);
}); });
} }

View File

@ -9,7 +9,7 @@
</ion-navbar> </ion-navbar>
</ion-header> </ion-header>
<ion-content padding class="addon-mod_lesson-password-modal"> <ion-content padding class="addon-mod_lesson-password-modal">
<form ion-list (ngSubmit)="submitPassword($event, passwordinput)"> <form ion-list (ngSubmit)="submitPassword($event, passwordinput)" #passwordForm>
<ion-item> <ion-item>
<core-show-password item-content [name]="'password'"> <core-show-password item-content [name]="'password'">
<ion-label>{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label> <ion-label>{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label>

View File

@ -12,8 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component } from '@angular/core'; import { Component, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, ViewController } from 'ionic-angular'; import { IonicPage, ViewController } from 'ionic-angular';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
/** /**
* Modal that asks the password for a lesson. * Modal that asks the password for a lesson.
@ -24,8 +27,12 @@ import { IonicPage, ViewController } from 'ionic-angular';
templateUrl: 'password-modal.html', templateUrl: 'password-modal.html',
}) })
export class AddonModLessonPasswordModalPage { export class AddonModLessonPasswordModalPage {
@ViewChild('passwordForm') formElement: ElementRef;
constructor(protected viewCtrl: ViewController) { } constructor(protected viewCtrl: ViewController,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider,
protected domUtils: CoreDomUtilsProvider) { }
/** /**
* Send the password back. * Send the password back.
@ -37,6 +44,8 @@ export class AddonModLessonPasswordModalPage {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false, this.sitesProvider.getCurrentSiteId());
this.viewCtrl.dismiss(password.value); this.viewCtrl.dismiss(password.value);
} }
@ -44,6 +53,8 @@ export class AddonModLessonPasswordModalPage {
* Close modal. * Close modal.
*/ */
closeModal(): void { closeModal(): void {
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
this.viewCtrl.dismiss(); this.viewCtrl.dismiss();
} }
} }

View File

@ -38,7 +38,7 @@
<!-- Question page. --> <!-- Question page. -->
<!-- We need to set ngIf loaded to make formGroup directive restart every time a page changes, see MOBILE-2540. --> <!-- We need to set ngIf loaded to make formGroup directive restart every time a page changes, see MOBILE-2540. -->
<form *ngIf="question && loaded" ion-list [formGroup]="questionForm"> <form *ngIf="question && loaded" ion-list [formGroup]="questionForm" #questionFormEl>
<ion-item-divider text-wrap *ngIf="pageContent"> <ion-item-divider text-wrap *ngIf="pageContent">
<core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="pageContent" contextLevel="module" [contextInstanceId]="lesson.coursemodule" [courseId]="courseId"></core-format-text> <core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="pageContent" contextLevel="module" [contextInstanceId]="lesson.coursemodule" [courseId]="courseId"></core-format-text>
</ion-item-divider> </ion-item-divider>
@ -57,7 +57,7 @@
<!-- Essay. --> <!-- Essay. -->
<ng-container *ngSwitchCase="'essay'"> <ng-container *ngSwitchCase="'essay'">
<ion-item *ngIf="question.textarea"> <ion-item *ngIf="question.textarea">
<core-rich-text-editor item-content placeholder="{{ 'addon.mod_lesson.youranswer' | translate }}" [control]="question.control" [component]="component" [componentId]="lesson.coursemodule"></core-rich-text-editor> <core-rich-text-editor item-content placeholder="{{ 'addon.mod_lesson.youranswer' | translate }}" [control]="question.control" [component]="component" [componentId]="lesson.coursemodule" [autoSave]="true" contextLevel="module" [contextInstanceId]="lesson.coursemodule" elementId="answer_editor"></core-rich-text-editor>
</ion-item> </ion-item>
<ion-item text-wrap *ngIf="!question.textarea && question.useranswer"> <ion-item text-wrap *ngIf="!question.textarea && question.useranswer">
<p class="item-heading">{{ 'addon.mod_lesson.youranswer' | translate }}</p> <p class="item-heading">{{ 'addon.mod_lesson.youranswer' | translate }}</p>

View File

@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModLessonPlayerPage } from './player'; import { AddonModLessonPlayerPage } from './player';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -26,6 +27,7 @@ import { AddonModLessonPlayerPage } from './player';
imports: [ imports: [
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule, CoreDirectivesModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModLessonPlayerPage), IonicPageModule.forChild(AddonModLessonPlayerPage),
TranslateModule.forChild() TranslateModule.forChild()
], ],

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef, ElementRef } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { IonicPage, NavParams, Content, PopoverController, ModalController, Modal, NavController } from 'ionic-angular'; import { IonicPage, NavParams, Content, PopoverController, ModalController, Modal, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -41,6 +41,7 @@ import { AddonModLessonHelperProvider } from '../../providers/helper';
}) })
export class AddonModLessonPlayerPage implements OnInit, OnDestroy { export class AddonModLessonPlayerPage implements OnInit, OnDestroy {
@ViewChild(Content) content: Content; @ViewChild(Content) content: Content;
@ViewChild('questionFormEl') formElement: ElementRef;
component = AddonModLessonProvider.COMPONENT; component = AddonModLessonProvider.COMPONENT;
LESSON_EOL = AddonModLessonProvider.LESSON_EOL; LESSON_EOL = AddonModLessonProvider.LESSON_EOL;
@ -136,19 +137,19 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy {
* *
* @return Resolved if we can leave it, rejected if not. * @return Resolved if we can leave it, rejected if not.
*/ */
ionViewCanLeave(): boolean | Promise<void> { async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) { if (this.forceLeave) {
return true; return;
} }
if (this.question && !this.eolData && !this.processData && this.originalData) { if (this.question && !this.eolData && !this.processData && this.originalData) {
// Question shown. Check if there is any change. // Question shown. Check if there is any change.
if (!this.utils.basicLeftCompare(this.questionForm.getRawValue(), this.originalData, 3)) { if (!this.utils.basicLeftCompare(this.questionForm.getRawValue(), this.originalData, 3)) {
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
} }
} }
return Promise.resolve(); this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
} }
/** /**
@ -540,15 +541,21 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy {
* Process a page, sending some data. * Process a page, sending some data.
* *
* @param data The data to send. * @param data The data to send.
* @param formSubmitted Whether a form was submitted.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected processPage(data: any): Promise<any> { protected processPage(data: any, formSubmitted?: boolean): Promise<any> {
this.loaded = false; this.loaded = false;
const args = [this.lesson, this.courseId, this.pageData, data, this.password, this.review, this.offline, this.accessInfo, const args = [this.lesson, this.courseId, this.pageData, data, this.password, this.review, this.offline, this.accessInfo,
this.jumps]; this.jumps];
return this.callFunction(this.lessonProvider.processPage.bind(this.lessonProvider), args, 6, 8).then((result) => { return this.callFunction(this.lessonProvider.processPage.bind(this.lessonProvider), args, 6, 8).then((result) => {
if (formSubmitted) {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, result.sent,
this.sitesProvider.getCurrentSiteId());
}
if (!this.offline && !this.review && this.lessonProvider.isLessonOffline(this.lesson)) { if (!this.offline && !this.review && this.lessonProvider.isLessonOffline(this.lesson)) {
// Lesson allows offline and the user changed some data in server. Update cached data. // Lesson allows offline and the user changed some data in server. Update cached data.
const retake = this.accessInfo.attemptscount; const retake = this.accessInfo.attemptscount;
@ -637,7 +644,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy {
// Use getRawValue to include disabled values. // Use getRawValue to include disabled values.
const data = this.lessonHelper.prepareQuestionData(this.question, this.questionForm.getRawValue()); const data = this.lessonHelper.prepareQuestionData(this.question, this.questionForm.getRawValue());
this.processPage(data).finally(() => { this.processPage(data, true).finally(() => {
this.loaded = true; this.loaded = true;
}); });
} }

View File

@ -3089,6 +3089,7 @@ export class AddonModLessonProvider {
result.warnings = []; result.warnings = [];
result.displaymenu = pageData.displaymenu; // Keep the same value since we can't calculate it in offline. result.displaymenu = pageData.displaymenu; // Keep the same value since we can't calculate it in offline.
result.messages = this.getPageProcessMessages(lesson, accessInfo, result, review, jumps); result.messages = this.getPageProcessMessages(lesson, accessInfo, result, review, jumps);
result.sent = false;
Object.assign(result, calculatedData); Object.assign(result, calculatedData);
return result; return result;
@ -3104,6 +3105,8 @@ export class AddonModLessonProvider {
review: review review: review
}, this.sitesProvider.getCurrentSiteId()); }, this.sitesProvider.getCurrentSiteId());
response.sent = true;
return response; return response;
}); });
} }

View File

@ -45,7 +45,7 @@
</div> </div>
<!-- Questions --> <!-- Questions -->
<form name="addon-mod_quiz-player-form" *ngIf="questions && questions.length && !quizAborted && !showSummary"> <form name="addon-mod_quiz-player-form" *ngIf="questions && questions.length && !quizAborted && !showSummary" #quizForm>
<div *ngFor="let question of questions"> <div *ngFor="let question of questions">
<ion-card id="addon-mod_quiz-question-{{question.slot}}"> <ion-card id="addon-mod_quiz-question-{{question.slot}}">
<!-- "Header" of the question. --> <!-- "Header" of the question. -->

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef, ViewChildren, QueryList } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef, ViewChildren, QueryList, ElementRef } from '@angular/core';
import { IonicPage, NavParams, Content, PopoverController, ModalController, Modal, NavController } from 'ionic-angular'; import { IonicPage, NavParams, Content, PopoverController, ModalController, Modal, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
@ -41,6 +41,7 @@ import { Subscription } from 'rxjs';
export class AddonModQuizPlayerPage implements OnInit, OnDestroy { export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
@ViewChild(Content) content: Content; @ViewChild(Content) content: Content;
@ViewChildren(CoreQuestionComponent) questionComponents: QueryList<CoreQuestionComponent>; @ViewChildren(CoreQuestionComponent) questionComponents: QueryList<CoreQuestionComponent>;
@ViewChild('quizForm') formElement: ElementRef;
quiz: any; // The quiz the attempt belongs to. quiz: any; // The quiz the attempt belongs to.
attempt: any; // The attempt being attempted. attempt: any; // The attempt being attempted.
@ -136,26 +137,28 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
* *
* @return Resolved if we can leave it, rejected if not. * @return Resolved if we can leave it, rejected if not.
*/ */
ionViewCanLeave(): boolean | Promise<void> { async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) { if (this.forceLeave) {
return true; return;
} }
if (this.questions && this.questions.length && !this.showSummary) { if (this.questions && this.questions.length && !this.showSummary) {
// Save answers. // Save answers.
const modal = this.domUtils.showModalLoading('core.sending', true); const modal = this.domUtils.showModalLoading('core.sending', true);
return this.processAttempt(false, false).catch(() => { try {
await this.processAttempt(false, false);
} catch (error) {
// Save attempt failed. Show confirmation. // Save attempt failed. Show confirmation.
modal.dismiss(); modal.dismiss();
return this.domUtils.showConfirm(this.translate.instant('addon.mod_quiz.confirmleavequizonerror')); await this.domUtils.showConfirm(this.translate.instant('addon.mod_quiz.confirmleavequizonerror'));
}).finally(() => {
modal.dismiss();
});
}
return Promise.resolve(); this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
} finally {
modal.dismiss();
}
}
} }
/** /**
@ -585,6 +588,9 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
// Answers saved, cancel auto save. // Answers saved, cancel auto save.
this.autoSave.cancelAutoSave(); this.autoSave.cancelAutoSave();
this.autoSave.hideAutoSaveError(); this.autoSave.hideAutoSaveError();
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, !this.offline,
this.sitesProvider.getCurrentSiteId());
}); });
} }

View File

@ -10,7 +10,7 @@
</ion-header> </ion-header>
<ion-content padding class="addon-mod_quiz-preflight-modal"> <ion-content padding class="addon-mod_quiz-preflight-modal">
<core-loading [hideUntil]="loaded"> <core-loading [hideUntil]="loaded">
<form ion-list [formGroup]="preflightForm" (ngSubmit)="sendData($event)"> <form ion-list [formGroup]="preflightForm" (ngSubmit)="sendData($event)" #preflightFormEl>
<!-- Access rules. --> <!-- Access rules. -->
<ng-container *ngFor="let data of accessRulesData; let last = last"> <ng-container *ngFor="let data of accessRulesData; let last = last">
<core-dynamic-component [component]="data.component" [data]="data.data"> <core-dynamic-component [component]="data.component" [data]="data.data">

View File

@ -12,10 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, Injector, ViewChild } from '@angular/core'; import { Component, OnInit, Injector, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, ViewController, NavParams, Content } from 'ionic-angular'; import { IonicPage, ViewController, NavParams, Content } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate'; import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate';
@ -31,6 +32,7 @@ import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-del
export class AddonModQuizPreflightModalPage implements OnInit { export class AddonModQuizPreflightModalPage implements OnInit {
@ViewChild(Content) content: Content; @ViewChild(Content) content: Content;
@ViewChild('preflightFormEl') formElement: ElementRef;
preflightForm: FormGroup; preflightForm: FormGroup;
title: string; title: string;
@ -43,9 +45,15 @@ export class AddonModQuizPreflightModalPage implements OnInit {
protected siteId: string; protected siteId: string;
protected rules: string[]; protected rules: string[];
constructor(params: NavParams, fb: FormBuilder, translate: TranslateService, sitesProvider: CoreSitesProvider, constructor(params: NavParams,
protected viewCtrl: ViewController, protected accessRuleDelegate: AddonModQuizAccessRuleDelegate, fb: FormBuilder,
protected injector: Injector, protected domUtils: CoreDomUtilsProvider) { translate: TranslateService,
sitesProvider: CoreSitesProvider,
protected viewCtrl: ViewController,
protected accessRuleDelegate: AddonModQuizAccessRuleDelegate,
protected injector: Injector,
protected domUtils: CoreDomUtilsProvider,
protected eventsProvider: CoreEventsProvider) {
this.title = params.get('title') || translate.instant('addon.mod_quiz.startattempt'); this.title = params.get('title') || translate.instant('addon.mod_quiz.startattempt');
this.quiz = params.get('quiz'); this.quiz = params.get('quiz');
@ -112,6 +120,8 @@ export class AddonModQuizPreflightModalPage implements OnInit {
this.domUtils.showErrorModal('core.errorinvalidform', true); this.domUtils.showErrorModal('core.errorinvalidform', true);
} }
} else { } else {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false, this.siteId);
this.viewCtrl.dismiss(this.preflightForm.value); this.viewCtrl.dismiss(this.preflightForm.value);
} }
} }
@ -120,6 +130,8 @@ export class AddonModQuizPreflightModalPage implements OnInit {
* Close modal. * Close modal.
*/ */
closeModal(): void { closeModal(): void {
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.siteId);
this.viewCtrl.dismiss(); this.viewCtrl.dismiss();
} }
} }

View File

@ -11,13 +11,13 @@
</ion-header> </ion-header>
<ion-content> <ion-content>
<core-loading [hideUntil]="loaded"> <core-loading [hideUntil]="loaded">
<form ion-list [formGroup]="pageForm"> <form ion-list [formGroup]="pageForm" #editPageForm>
<ion-item text-wrap *ngIf="canEditTitle" class="item-title"> <ion-item text-wrap *ngIf="canEditTitle" class="item-title">
<ion-input name="title" type="text" [placeholder]="'addon.mod_wiki.newpagetitle' | translate" [formControlName]="'title'"></ion-input> <ion-input name="title" type="text" [placeholder]="'addon.mod_wiki.newpagetitle' | translate" [formControlName]="'title'"></ion-input>
</ion-item> </ion-item>
<ion-item> <ion-item>
<core-rich-text-editor item-content [control]="contentControl" [placeholder]="'core.content' | translate" name="wiki_page_content" [component]="component" [componentId]="componentId"></core-rich-text-editor> <core-rich-text-editor item-content [control]="contentControl" [placeholder]="'core.content' | translate" name="wiki_page_content" [component]="component" [componentId]="componentId" [autoSave]="true" contextLevel="module" [contextInstanceId]="module.id || 0" elementId="newcontent_editor" [draftExtraParams]="editorExtraParams"></core-rich-text-editor>
</ion-item> </ion-item>
<ion-item *ngIf="wrongVersionLock" text-center class="addon-mod_wiki-wrongversionlock" > <ion-item *ngIf="wrongVersionLock" text-center class="addon-mod_wiki-wrongversionlock" >

View File

@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModWikiEditPage } from './edit'; import { AddonModWikiEditPage } from './edit';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -26,6 +27,7 @@ import { AddonModWikiEditPage } from './edit';
imports: [ imports: [
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule, CoreDirectivesModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModWikiEditPage), IonicPageModule.forChild(AddonModWikiEditPage),
TranslateModule.forChild() TranslateModule.forChild()
], ],

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { FormControl, FormGroup, FormBuilder } from '@angular/forms'; import { FormControl, FormGroup, FormBuilder } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -37,6 +37,8 @@ import { AddonModWikiSyncProvider, AddonModWikiSyncSubwikiResult } from '../../p
}) })
export class AddonModWikiEditPage implements OnInit, OnDestroy { export class AddonModWikiEditPage implements OnInit, OnDestroy {
@ViewChild('editPageForm') formElement: ElementRef;
title: string; // Title to display. title: string; // Title to display.
pageForm: FormGroup; // The form group. pageForm: FormGroup; // The form group.
contentControl: FormControl; // The FormControl for the page content. contentControl: FormControl; // The FormControl for the page content.
@ -45,6 +47,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy {
component = AddonModWikiProvider.COMPONENT; // Component to link the files to. component = AddonModWikiProvider.COMPONENT; // Component to link the files to.
componentId: number; // Component ID to link the files to. componentId: number; // Component ID to link the files to.
wrongVersionLock: boolean; // Whether the page lock doesn't match the initial one. wrongVersionLock: boolean; // Whether the page lock doesn't match the initial one.
editorExtraParams: {[name: string]: any} = {};
protected module: any; // Wiki module instance. protected module: any; // Wiki module instance.
protected courseId: number; // Course the wiki belongs to. protected courseId: number; // Course the wiki belongs to.
@ -101,6 +104,20 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy {
// Block the wiki so it cannot be synced. // Block the wiki so it cannot be synced.
this.syncProvider.blockOperation(this.component, this.blockId); this.syncProvider.blockOperation(this.component, this.blockId);
if (!this.module.id) {
this.editorExtraParams.type = 'wiki';
}
if (this.pageId) {
this.editorExtraParams.pageid = this.pageId;
if (this.section) {
this.editorExtraParams.section = this.section;
}
} else if (pageTitle) {
this.editorExtraParams.pagetitle = pageTitle;
}
} }
/** /**
@ -329,17 +346,17 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy {
* *
* @return Resolved if we can leave it, rejected if not. * @return Resolved if we can leave it, rejected if not.
*/ */
ionViewCanLeave(): boolean | Promise<void> { async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) { if (this.forceLeave) {
return true; return;
} }
// Check if data has changed. // Check if data has changed.
if (this.hasDataChanged()) { if (this.hasDataChanged()) {
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
} }
return true; this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
} }
/** /**
@ -408,6 +425,10 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy {
if (this.editing) { if (this.editing) {
// Edit existing page. // Edit existing page.
promise = this.wikiProvider.editPage(this.pageId, text, this.section).then(() => { promise = this.wikiProvider.editPage(this.pageId, text, this.section).then(() => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, true,
this.sitesProvider.getCurrentSiteId());
// Invalidate page since it changed. // Invalidate page since it changed.
return this.wikiProvider.invalidatePage(this.pageId).then(() => { return this.wikiProvider.invalidatePage(this.pageId).then(() => {
return this.gotoPage(title); return this.gotoPage(title);
@ -441,6 +462,10 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy {
let wikiId = this.wikiId || (this.module && this.module.instance); let wikiId = this.wikiId || (this.module && this.module.instance);
return this.wikiProvider.newPage(title, text, this.subwikiId, wikiId, this.userId, this.groupId).then((id) => { return this.wikiProvider.newPage(title, text, this.subwikiId, wikiId, this.userId, this.groupId).then((id) => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, id > 0,
this.sitesProvider.getCurrentSiteId());
if (id > 0) { if (id > 0) {
// Page was created, get its data and go to the page. // Page was created, get its data and go to the page.
this.pageId = id; this.pageId = id;

View File

@ -1,6 +1,6 @@
<h3 padding>{{ 'addon.mod_workshop.assessmentform' | translate }}</h3> <h3 padding>{{ 'addon.mod_workshop.assessmentform' | translate }}</h3>
<form name="mma-mod_workshop-assessment-form"> <form name="mma-mod_workshop-assessment-form" #assessmentForm>
<core-loading [hideUntil]="assessmentStrategyLoaded"> <core-loading [hideUntil]="assessmentStrategyLoaded">
<ng-container *ngIf="componentClass && assessmentStrategyLoaded"> <ng-container *ngIf="componentClass && assessmentStrategyLoaded">
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component> <core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
@ -16,7 +16,7 @@
</ion-item> </ion-item>
<ion-item stacked *ngIf="edit"> <ion-item stacked *ngIf="edit">
<ion-label stacked [core-mark-required]="overallFeedkbackRequired">{{ 'addon.mod_workshop.feedbackauthor' | translate }}</ion-label> <ion-label stacked [core-mark-required]="overallFeedkbackRequired">{{ 'addon.mod_workshop.feedbackauthor' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="feedbackControl" [component]="component" [componentId]="workshop.coursemodule" (contentChanged)="onFeedbackChange($event)"></core-rich-text-editor> <core-rich-text-editor item-content [control]="feedbackControl" [component]="component" [componentId]="workshop.coursemodule" [autoSave]="true" contextLevel="module" [contextInstanceId]="workshop.coursemodule" elementId="feedbackauthor_editor" [draftExtraParams]="{asid: assessmentId}" (contentChanged)="onFeedbackChange($event)"></core-rich-text-editor>
<core-input-errors item-content *ngIf="overallFeedkbackRequired && fieldErrors && fieldErrors['feedbackauthor']" [errorText]="fieldErrors['feedbackauthor']"></core-input-errors> <core-input-errors item-content *ngIf="overallFeedkbackRequired && fieldErrors && fieldErrors['feedbackauthor']" [errorText]="fieldErrors['feedbackauthor']"></core-input-errors>
</ion-item> </ion-item>
<core-attachments *ngIf="edit && workshop.overallfeedbackfiles" [files]="data.assessment.feedbackattachmentfiles" [maxSize]="workshop.overallfeedbackmaxbytes" <core-attachments *ngIf="edit && workshop.overallfeedbackfiles" [files]="data.assessment.feedbackattachmentfiles" [maxSize]="workshop.overallfeedbackmaxbytes"

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, Input, OnInit, Injector } from '@angular/core'; import { Component, Input, OnInit, Injector, ViewChild, ElementRef } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreSyncProvider } from '@providers/sync'; import { CoreSyncProvider } from '@providers/sync';
@ -44,6 +44,8 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit {
@Input() strategy: string; @Input() strategy: string;
@Input() edit?: boolean; @Input() edit?: boolean;
@ViewChild('assessmentForm') formElement: ElementRef;
componentClass: any; componentClass: any;
data = { data = {
workshopId: 0, workshopId: 0,
@ -292,7 +294,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit {
// Save assessment in offline. // Save assessment in offline.
return this.workshopOffline.saveAssessment(this.workshop.id, this.assessmentId, this.workshop.course, return this.workshopOffline.saveAssessment(this.workshop.id, this.assessmentId, this.workshop.course,
assessmentData).then(() => { assessmentData).then(() => {
// Don't return anything. return false;
}); });
} }
@ -301,6 +303,9 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit {
return this.workshopProvider.updateAssessment(this.workshop.id, this.assessmentId, this.workshop.course, return this.workshopProvider.updateAssessment(this.workshop.id, this.assessmentId, this.workshop.course,
assessmentData, false, allowOffline); assessmentData, false, allowOffline);
}).then((grade) => { }).then((grade) => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, !!grade, this.sitesProvider.getCurrentSiteId());
const promises = []; const promises = [];
// If sent to the server, invalidate and clean. // If sent to the server, invalidate and clean.

View File

@ -24,6 +24,7 @@ import { AddonModWorkshopIndexComponent } from './index/index';
import { AddonModWorkshopSubmissionComponent } from './submission/submission'; import { AddonModWorkshopSubmissionComponent } from './submission/submission';
import { AddonModWorkshopAssessmentComponent } from './assessment/assessment'; import { AddonModWorkshopAssessmentComponent } from './assessment/assessment';
import { AddonModWorkshopAssessmentStrategyComponent } from './assessment-strategy/assessment-strategy'; import { AddonModWorkshopAssessmentStrategyComponent } from './assessment-strategy/assessment-strategy';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -39,7 +40,8 @@ import { AddonModWorkshopAssessmentStrategyComponent } from './assessment-strate
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule, CoreDirectivesModule,
CorePipesModule, CorePipesModule,
CoreCourseComponentsModule CoreCourseComponentsModule,
CoreEditorComponentsModule,
], ],
providers: [ providers: [
], ],

View File

@ -38,7 +38,7 @@
<addon-mod-workshop-assessment-strategy *ngIf="assessment && assessmentId && showGrade(assessment.grade) && workshop && access" [workshop]="workshop" [access]="access" [assessmentId]="assessmentId" [userId]="profile && profile.id" [strategy]="strategy"></addon-mod-workshop-assessment-strategy> <addon-mod-workshop-assessment-strategy *ngIf="assessment && assessmentId && showGrade(assessment.grade) && workshop && access" [workshop]="workshop" [access]="access" [assessmentId]="assessmentId" [userId]="profile && profile.id" [strategy]="strategy"></addon-mod-workshop-assessment-strategy>
<form ion-list [formGroup]="evaluateForm" *ngIf="evaluating"> <form ion-list [formGroup]="evaluateForm" *ngIf="evaluating" #evaluateFormEl>
<ion-item text-wrap> <ion-item text-wrap>
<h2>{{ 'addon.mod_workshop.assessmentsettings' | translate }}</h2> <h2>{{ 'addon.mod_workshop.assessmentsettings' | translate }}</h2>
</ion-item> </ion-item>
@ -60,7 +60,7 @@
</ion-item> </ion-item>
<ion-item *ngIf="access.canoverridegrades"> <ion-item *ngIf="access.canoverridegrades">
<ion-label stacked>{{ 'addon.mod_workshop.feedbackreviewer' | translate }}</ion-label> <ion-label stacked>{{ 'addon.mod_workshop.feedbackreviewer' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="evaluateForm.controls['text']" formControlName="text"></core-rich-text-editor> <core-rich-text-editor item-content [control]="evaluateForm.controls['text']" formControlName="text" [autoSave]="true" contextLevel="module" [contextInstanceId]="workshop.coursemodule" elementId="feedbackreviewer_editor" [draftExtraParams]="{asid: assessmentId}"></core-rich-text-editor>
</ion-item> </ion-item>
</form> </form>
<ion-list *ngIf="!evaluating && evaluate && evaluate.text"> <ion-list *ngIf="!evaluating && evaluate && evaluate.text">

View File

@ -19,6 +19,7 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModWorkshopComponentsModule } from '../../components/components.module'; import { AddonModWorkshopComponentsModule } from '../../components/components.module';
import { AddonModWorkshopAssessmentPage } from './assessment'; import { AddonModWorkshopAssessmentPage } from './assessment';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -28,6 +29,7 @@ import { AddonModWorkshopAssessmentPage } from './assessment';
CoreDirectivesModule, CoreDirectivesModule,
CoreComponentsModule, CoreComponentsModule,
AddonModWorkshopComponentsModule, AddonModWorkshopComponentsModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModWorkshopAssessmentPage), IonicPageModule.forChild(AddonModWorkshopAssessmentPage),
TranslateModule.forChild() TranslateModule.forChild()
], ],

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavParams, NavController } from 'ionic-angular'; import { IonicPage, NavParams, NavController } from 'ionic-angular';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -39,6 +39,8 @@ import { AddonModWorkshopSyncProvider } from '../../providers/sync';
}) })
export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy { export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy {
@ViewChild('evaluateFormEl') formElement: ElementRef;
assessment: any; assessment: any;
submission: any; submission: any;
profile: any; profile: any;
@ -118,17 +120,19 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy {
* *
* @return Resolved if we can leave it, rejected if not. * @return Resolved if we can leave it, rejected if not.
*/ */
ionViewCanLeave(): boolean | Promise<void> { async ionViewCanLeave(): Promise<void> {
if (this.forceLeave || !this.evaluating) { if (this.forceLeave || !this.evaluating) {
return true; return;
} }
if (!this.hasEvaluationChanged()) { if (!this.hasEvaluationChanged()) {
return Promise.resolve(); return;
} }
// Show confirmation if some data has been modified. // Show confirmation if some data has been modified.
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.siteId);
} }
/** /**
@ -340,7 +344,10 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy {
// Try to send it to server. // Try to send it to server.
return this.workshopProvider.evaluateAssessment(this.workshopId, this.assessmentId, this.courseId, inputData.text, return this.workshopProvider.evaluateAssessment(this.workshopId, this.assessmentId, this.courseId, inputData.text,
inputData.weight, inputData.grade).then(() => { inputData.weight, inputData.grade).then((result) => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, !!result, this.siteId);
const data = { const data = {
workshopId: this.workshopId, workshopId: this.workshopId,
assessmentId: this.assessmentId, assessmentId: this.assessmentId,

View File

@ -10,7 +10,7 @@
</ion-header> </ion-header>
<ion-content> <ion-content>
<core-loading [hideUntil]="loaded"> <core-loading [hideUntil]="loaded">
<form ion-list [formGroup]="editForm" *ngIf="workshop"> <form ion-list [formGroup]="editForm" *ngIf="workshop" #editFormEl>
<ion-item text-wrap> <ion-item text-wrap>
<ion-label stacked core-mark-required="true">{{ 'addon.mod_workshop.submissiontitle' | translate }}</ion-label> <ion-label stacked core-mark-required="true">{{ 'addon.mod_workshop.submissiontitle' | translate }}</ion-label>
<ion-input name="title" type="text" [placeholder]="'addon.mod_workshop.submissiontitle' | translate" formControlName="title"></ion-input> <ion-input name="title" type="text" [placeholder]="'addon.mod_workshop.submissiontitle' | translate" formControlName="title"></ion-input>
@ -18,7 +18,7 @@
<ion-item *ngIf="textAvailable"> <ion-item *ngIf="textAvailable">
<ion-label stacked [core-mark-required]="textRequired">{{ 'addon.mod_workshop.submissioncontent' | translate }}</ion-label> <ion-label stacked [core-mark-required]="textRequired">{{ 'addon.mod_workshop.submissioncontent' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="editForm.controls['content']" formControlName="content" [placeholder]="'addon.mod_workshop.submissioncontent' | translate" name="content" [component]="component" [componentId]="componentId"></core-rich-text-editor> <core-rich-text-editor item-content [control]="editForm.controls['content']" formControlName="content" [placeholder]="'addon.mod_workshop.submissioncontent' | translate" name="content" [component]="component" [componentId]="componentId" [autoSave]="true" contextLevel="module" [contextInstanceId]="module.id" elementId="content_editor" [draftExtraParams]="editorExtraParams"></core-rich-text-editor>
</ion-item> </ion-item>
<core-attachments *ngIf="fileAvailable" [files]="submission.attachmentfiles" [maxSize]="workshop.maxbytes" [maxSubmissions]="workshop.nattachments" [component]="component" [componentId]="workshop.coursemodule" allowOffline="true" [acceptedTypes]="workshop.submissionfiletypes" [required]="fileRequired"></core-attachments> <core-attachments *ngIf="fileAvailable" [files]="submission.attachmentfiles" [maxSize]="workshop.maxbytes" [maxSubmissions]="workshop.nattachments" [component]="component" [componentId]="workshop.coursemodule" allowOffline="true" [acceptedTypes]="workshop.submissionfiletypes" [required]="fileRequired"></core-attachments>

View File

@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModWorkshopEditSubmissionPage } from './edit-submission'; import { AddonModWorkshopEditSubmissionPage } from './edit-submission';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -26,6 +27,7 @@ import { AddonModWorkshopEditSubmissionPage } from './edit-submission';
imports: [ imports: [
CoreDirectivesModule, CoreDirectivesModule,
CoreComponentsModule, CoreComponentsModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModWorkshopEditSubmissionPage), IonicPageModule.forChild(AddonModWorkshopEditSubmissionPage),
TranslateModule.forChild() TranslateModule.forChild()
], ],

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavParams, NavController } from 'ionic-angular'; import { IonicPage, NavParams, NavController } from 'ionic-angular';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -37,6 +37,8 @@ import { AddonModWorkshopOfflineProvider } from '../../providers/offline';
}) })
export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
@ViewChild('editFormEl') formElement: ElementRef;
module: any; module: any;
courseId: number; courseId: number;
access: any; access: any;
@ -51,6 +53,7 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
component = AddonModWorkshopProvider.COMPONENT; component = AddonModWorkshopProvider.COMPONENT;
componentId: number; componentId: number;
editForm: FormGroup; // The form group. editForm: FormGroup; // The form group.
editorExtraParams: {[name: string]: any} = {};
protected workshopId: number; protected workshopId: number;
protected submissionId: number; protected submissionId: number;
@ -86,6 +89,10 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
this.editForm = new FormGroup({}); this.editForm = new FormGroup({});
this.editForm.addControl('title', this.fb.control('', Validators.required)); this.editForm.addControl('title', this.fb.control('', Validators.required));
this.editForm.addControl('content', this.fb.control('')); this.editForm.addControl('content', this.fb.control(''));
if (this.submissionId) {
this.editorExtraParams.id = this.submissionId;
}
} }
/** /**
@ -105,27 +112,23 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
* *
* @return Resolved if we can leave it, rejected if not. * @return Resolved if we can leave it, rejected if not.
*/ */
ionViewCanLeave(): boolean | Promise<void> { async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) { if (this.forceLeave) {
return true; return;
} }
let promise;
// Check if data has changed. // Check if data has changed.
if (!this.hasDataChanged()) { if (this.hasDataChanged()) {
promise = Promise.resolve();
} else {
// Show confirmation if some data has been modified. // Show confirmation if some data has been modified.
promise = this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
} }
return promise.then(() => {
if (this.submission.attachmentfiles) { if (this.submission.attachmentfiles) {
// Delete the local files from the tmp folder. // Delete the local files from the tmp folder.
this.fileUploaderProvider.clearTmpFiles(this.submission.attachmentfiles); this.fileUploaderProvider.clearTmpFiles(this.submission.attachmentfiles);
} }
});
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.siteId);
} }
/** /**
@ -347,7 +350,7 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
// Save submission in offline. // Save submission in offline.
return this.workshopOffline.saveSubmission(this.workshopId, this.courseId, inputData.title, return this.workshopOffline.saveSubmission(this.workshopId, this.courseId, inputData.title,
inputData.content, attachmentsId, submissionId, 'update').then(() => { inputData.content, attachmentsId, submissionId, 'update').then(() => {
// Don't return anything. return false;
}); });
} }
@ -361,7 +364,7 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
// Save submission in offline. // Save submission in offline.
return this.workshopOffline.saveSubmission(this.workshopId, this.courseId, inputData.title, inputData.content, return this.workshopOffline.saveSubmission(this.workshopId, this.courseId, inputData.title, inputData.content,
attachmentsId, submissionId, 'add').then(() => { attachmentsId, submissionId, 'add').then(() => {
// Don't return anything. return false;
}); });
} }
@ -370,6 +373,9 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
return this.workshopProvider.addSubmission(this.workshopId, this.courseId, inputData.title, inputData.content, return this.workshopProvider.addSubmission(this.workshopId, this.courseId, inputData.title, inputData.content,
attachmentsId, undefined, submissionId, allowOffline); attachmentsId, undefined, submissionId, allowOffline);
}).then((newSubmissionId) => { }).then((newSubmissionId) => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, !!newSubmissionId, this.siteId);
const data = { const data = {
workshopId: this.workshopId, workshopId: this.workshopId,
cmId: this.module.cmid cmId: this.module.cmid

View File

@ -65,7 +65,7 @@
<addon-mod-workshop-assessment *ngFor="let reviewer of submissionInfo.reviewerof" [assessment]="reviewer" [courseId]="courseId" summary="true" [workshop]="workshop" [access]="access"></addon-mod-workshop-assessment> <addon-mod-workshop-assessment *ngFor="let reviewer of submissionInfo.reviewerof" [assessment]="reviewer" [courseId]="courseId" summary="true" [workshop]="workshop" [access]="access"></addon-mod-workshop-assessment>
</ion-list> </ion-list>
<form ion-list [formGroup]="feedbackForm" *ngIf="canAddFeedback"> <form ion-list [formGroup]="feedbackForm" *ngIf="canAddFeedback" #feedbackFormEl>
<ion-item text-wrap> <ion-item text-wrap>
<h2>{{ 'addon.mod_workshop.feedbackauthor' | translate }}</h2> <h2>{{ 'addon.mod_workshop.feedbackauthor' | translate }}</h2>
</ion-item> </ion-item>
@ -87,7 +87,7 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-label stacked>{{ 'addon.mod_workshop.feedbackauthor' | translate }}</ion-label> <ion-label stacked>{{ 'addon.mod_workshop.feedbackauthor' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="feedbackForm.controls['text']" formControlName="text"></core-rich-text-editor> <core-rich-text-editor item-content [control]="feedbackForm.controls['text']" formControlName="text" [autoSave]="true" contextLevel="module" [contextInstanceId]="module.id" elementId="feedbackauthor_editor" [draftExtraParams]="{id: submissionId}"></core-rich-text-editor>
</ion-item> </ion-item>
</form> </form>

View File

@ -19,6 +19,7 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModWorkshopComponentsModule } from '../../components/components.module'; import { AddonModWorkshopComponentsModule } from '../../components/components.module';
import { AddonModWorkshopSubmissionPage } from './submission'; import { AddonModWorkshopSubmissionPage } from './submission';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -28,6 +29,7 @@ import { AddonModWorkshopSubmissionPage } from './submission';
CoreDirectivesModule, CoreDirectivesModule,
CoreComponentsModule, CoreComponentsModule,
AddonModWorkshopComponentsModule, AddonModWorkshopComponentsModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModWorkshopSubmissionPage), IonicPageModule.forChild(AddonModWorkshopSubmissionPage),
TranslateModule.forChild() TranslateModule.forChild()
], ],

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, OnDestroy, Optional, ViewChild } from '@angular/core'; import { Component, OnInit, OnDestroy, Optional, ViewChild, ElementRef } from '@angular/core';
import { Content, IonicPage, NavParams, NavController } from 'ionic-angular'; import { Content, IonicPage, NavParams, NavController } from 'ionic-angular';
import { FormGroup, FormBuilder } from '@angular/forms'; import { FormGroup, FormBuilder } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -41,6 +41,7 @@ import { AddonModWorkshopSyncProvider } from '../../providers/sync';
export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy {
@ViewChild(AddonModWorkshopAssessmentStrategyComponent) assessmentStrategy: AddonModWorkshopAssessmentStrategyComponent; @ViewChild(AddonModWorkshopAssessmentStrategyComponent) assessmentStrategy: AddonModWorkshopAssessmentStrategyComponent;
@ViewChild('feedbackFormEl') formElement: ElementRef;
module: any; module: any;
workshop: any; workshop: any;
@ -143,14 +144,16 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy {
* *
* @return Resolved if we can leave it, rejected if not. * @return Resolved if we can leave it, rejected if not.
*/ */
ionViewCanLeave(): boolean | Promise<void> { async ionViewCanLeave(): Promise<void> {
const assessmentHasChanged = this.assessmentStrategy && this.assessmentStrategy.hasDataChanged(); const assessmentHasChanged = this.assessmentStrategy && this.assessmentStrategy.hasDataChanged();
if (this.forceLeave || (!this.hasEvaluationChanged() && !assessmentHasChanged)) { if (this.forceLeave || (!this.hasEvaluationChanged() && !assessmentHasChanged)) {
return true; return;
} }
// Show confirmation if some data has been modified. // Show confirmation if some data has been modified.
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.siteId);
} }
/** /**
@ -444,7 +447,10 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy {
// Try to send it to server. // Try to send it to server.
return this.workshopProvider.evaluateSubmission(this.workshopId, this.submissionId, this.courseId, inputData.text, return this.workshopProvider.evaluateSubmission(this.workshopId, this.submissionId, this.courseId, inputData.text,
inputData.published, inputData.grade).then(() => { inputData.published, inputData.grade).then((result) => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, !!result, this.siteId);
const data = { const data = {
workshopId: this.workshopId, workshopId: this.workshopId,
cmId: this.module.cmid, cmId: this.module.cmid,

View File

@ -9,7 +9,7 @@
</ion-navbar> </ion-navbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<form name="itemEdit" (ngSubmit)="addNote($event)"> <form name="itemEdit" (ngSubmit)="addNote($event)" #itemEdit>
<ion-item> <ion-item>
<ion-label>{{ 'addon.notes.publishstate' | translate }}</ion-label> <ion-label>{{ 'addon.notes.publishstate' | translate }}</ion-label>
<ion-select [(ngModel)]="type" name="publishState" interface="popover"> <ion-select [(ngModel)]="type" name="publishState" interface="popover">

View File

@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component } from '@angular/core'; import { Component, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, ViewController, NavParams } from 'ionic-angular'; import { IonicPage, ViewController, NavParams } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { AddonNotesProvider } from '../../providers/notes'; import { AddonNotesProvider } from '../../providers/notes';
@ -27,14 +29,22 @@ import { AddonNotesProvider } from '../../providers/notes';
templateUrl: 'add.html', templateUrl: 'add.html',
}) })
export class AddonNotesAddPage { export class AddonNotesAddPage {
@ViewChild('itemEdit') formElement: ElementRef;
userId: number; userId: number;
courseId: number; courseId: number;
type = 'personal'; type = 'personal';
text = ''; text = '';
processing = false; processing = false;
constructor(params: NavParams, private viewCtrl: ViewController, private appProvider: CoreAppProvider, constructor(params: NavParams,
private domUtils: CoreDomUtilsProvider, private notesProvider: AddonNotesProvider) { protected viewCtrl: ViewController,
protected appProvider: CoreAppProvider,
protected domUtils: CoreDomUtilsProvider,
protected notesProvider: AddonNotesProvider,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider) {
this.userId = params.get('userId'); this.userId = params.get('userId');
this.courseId = params.get('courseId'); this.courseId = params.get('courseId');
this.type = params.get('type') || 'personal'; this.type = params.get('type') || 'personal';
@ -54,6 +64,9 @@ export class AddonNotesAddPage {
// Freeze the add note button. // Freeze the add note button.
this.processing = true; this.processing = true;
this.notesProvider.addNote(this.userId, this.courseId, this.type, this.text).then((sent) => { this.notesProvider.addNote(this.userId, this.courseId, this.type, this.text).then((sent) => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, sent, this.sitesProvider.getCurrentSiteId());
this.viewCtrl.dismiss({type: this.type, sent: true}).finally(() => { this.viewCtrl.dismiss({type: this.type, sent: true}).finally(() => {
this.domUtils.showToast(sent ? 'addon.notes.eventnotecreated' : 'core.datastoredoffline', true, 3000); this.domUtils.showToast(sent ? 'addon.notes.eventnotecreated' : 'core.datastoredoffline', true, 3000);
}); });
@ -69,6 +82,8 @@ export class AddonNotesAddPage {
* Close modal. * Close modal.
*/ */
closeModal(): void { closeModal(): void {
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
this.viewCtrl.dismiss({type: this.type}); this.viewCtrl.dismiss({type: this.type});
} }
} }

View File

@ -11,7 +11,7 @@
<!-- Plain text textarea. --> <!-- Plain text textarea. -->
<ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name" aria-multiline="true" [ngModel]="question.textarea.text"></ion-textarea> <ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name" aria-multiline="true" [ngModel]="question.textarea.text"></ion-textarea>
<!-- Rich text editor. --> <!-- Rich text editor. -->
<core-rich-text-editor item-content *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}" [control]="formControl" [name]="question.textarea.name" [component]="component" [componentId]="componentId"></core-rich-text-editor> <core-rich-text-editor item-content *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}" [control]="formControl" [name]="question.textarea.name" [component]="component" [componentId]="componentId" [autoSave]="false"></core-rich-text-editor>
</ion-item> </ion-item>
<!-- Draft files not supported. --> <!-- Draft files not supported. -->

View File

@ -20,6 +20,7 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonQtypeEssayHandler } from './providers/handler'; import { AddonQtypeEssayHandler } from './providers/handler';
import { AddonQtypeEssayComponent } from './component/essay'; import { AddonQtypeEssayComponent } from './component/essay';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -29,7 +30,8 @@ import { AddonQtypeEssayComponent } from './component/essay';
IonicModule, IonicModule,
TranslateModule.forChild(), TranslateModule.forChild(),
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule CoreDirectivesModule,
CoreEditorComponentsModule,
], ],
providers: [ providers: [
AddonQtypeEssayHandler AddonQtypeEssayHandler

View File

@ -9,5 +9,5 @@
<span [core-mark-required]="field.required">{{ field.name }}</span> <span [core-mark-required]="field.required">{{ field.name }}</span>
<core-input-errors [control]="control"></core-input-errors> <core-input-errors [control]="control"></core-input-errors>
</ion-label> </ion-label>
<core-rich-text-editor item-content [control]="control" [placeholder]="field.name"></core-rich-text-editor> <core-rich-text-editor item-content [control]="control" [placeholder]="field.name" [autoSave]="true" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [elementId]="'profile_field_' + field.shortname"></core-rich-text-editor>
</ion-item> </ion-item>

View File

@ -20,6 +20,7 @@ import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-
import { AddonUserProfileFieldTextareaComponent } from './component/textarea'; import { AddonUserProfileFieldTextareaComponent } from './component/textarea';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -29,7 +30,8 @@ import { CoreDirectivesModule } from '@directives/directives.module';
IonicModule, IonicModule,
TranslateModule.forChild(), TranslateModule.forChild(),
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule CoreDirectivesModule,
CoreEditorComponentsModule,
], ],
providers: [ providers: [
AddonUserProfileFieldTextareaHandler AddonUserProfileFieldTextareaHandler

View File

@ -87,6 +87,7 @@ import { CoreTagModule } from '@core/tag/tag.module';
import { CoreFilterModule } from '@core/filter/filter.module'; import { CoreFilterModule } from '@core/filter/filter.module';
import { CoreH5PModule } from '@core/h5p/h5p.module'; import { CoreH5PModule } from '@core/h5p/h5p.module';
import { CoreSearchModule } from '@core/search/search.module'; import { CoreSearchModule } from '@core/search/search.module';
import { CoreEditorModule } from '@core/editor/editor.module';
// Addon modules. // Addon modules.
import { AddonBadgesModule } from '@addon/badges/badges.module'; import { AddonBadgesModule } from '@addon/badges/badges.module';
@ -235,6 +236,7 @@ export const WP_PROVIDER: any = null;
CoreFilterModule, CoreFilterModule,
CoreH5PModule, CoreH5PModule,
CoreSearchModule, CoreSearchModule,
CoreEditorModule,
AddonBadgesModule, AddonBadgesModule,
AddonBlogModule, AddonBlogModule,
AddonCalendarModule, AddonCalendarModule,

View File

@ -1482,6 +1482,8 @@
"core.downloaded": "Downloaded", "core.downloaded": "Downloaded",
"core.downloading": "Downloading", "core.downloading": "Downloading",
"core.edit": "Edit", "core.edit": "Edit",
"core.editor.autosavesucceeded": "Draft saved.",
"core.editor.textrecovered": "A draft version of this text was automatically restored.",
"core.emptysplit": "This page will appear blank if the left panel is empty or is loading.", "core.emptysplit": "This page will appear blank if the left panel is empty or is loading.",
"core.error": "Error", "core.error": "Error",
"core.errorchangecompletion": "An error occurred while changing the completion status. Please try again.", "core.errorchangecompletion": "An error occurred while changing the completion status. Please try again.",

View File

@ -39,7 +39,6 @@ import { CoreLocalFileComponent } from './local-file/local-file';
import { CoreSitePickerComponent } from './site-picker/site-picker'; import { CoreSitePickerComponent } from './site-picker/site-picker';
import { CoreTabsComponent } from './tabs/tabs'; import { CoreTabsComponent } from './tabs/tabs';
import { CoreTabComponent } from './tabs/tab'; import { CoreTabComponent } from './tabs/tab';
import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor';
import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons'; import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
import { CoreDynamicComponent } from './dynamic-component/dynamic-component'; import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
import { CoreSendMessageFormComponent } from './send-message-form/send-message-form'; import { CoreSendMessageFormComponent } from './send-message-form/send-message-form';
@ -79,7 +78,6 @@ import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
CoreSitePickerComponent, CoreSitePickerComponent,
CoreTabsComponent, CoreTabsComponent,
CoreTabComponent, CoreTabComponent,
CoreRichTextEditorComponent,
CoreNavBarButtonsComponent, CoreNavBarButtonsComponent,
CoreDynamicComponent, CoreDynamicComponent,
CoreSendMessageFormComponent, CoreSendMessageFormComponent,
@ -128,7 +126,6 @@ import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
CoreSitePickerComponent, CoreSitePickerComponent,
CoreTabsComponent, CoreTabsComponent,
CoreTabComponent, CoreTabComponent,
CoreRichTextEditorComponent,
CoreNavBarButtonsComponent, CoreNavBarButtonsComponent,
CoreDynamicComponent, CoreDynamicComponent,
CoreSendMessageFormComponent, CoreSendMessageFormComponent,

View File

@ -1,4 +1,4 @@
<form (ngSubmit)="changeName(newFileName, $event)"> <form (ngSubmit)="changeName(newFileName, $event)" #nameForm>
<a ion-item text-wrap stacked class="item-media" [class.item-input]="editMode" (click)="fileClicked($event)" detail-none> <a ion-item text-wrap stacked class="item-media" [class.item-input]="editMode" (click)="fileClicked($event)" detail-none>
<img [src]="fileIcon" alt="{{fileExtension}}" role="presentation" item-start /> <img [src]="fileIcon" alt="{{fileExtension}}" role="presentation" item-start />

View File

@ -12,8 +12,10 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, Input, Output, OnInit, EventEmitter } from '@angular/core'; import { Component, Input, Output, OnInit, EventEmitter, ViewChild, ElementRef } from '@angular/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreFileProvider } from '@providers/file'; import { CoreFileProvider } from '@providers/file';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
@ -38,6 +40,8 @@ export class CoreLocalFileComponent implements OnInit {
@Output() onRename?: EventEmitter<any>; // Will notify when the file is renamed. Receives the FileEntry as the param. @Output() onRename?: EventEmitter<any>; // Will notify when the file is renamed. Receives the FileEntry as the param.
@Output() onClick?: EventEmitter<void>; // Will notify when the file is clicked. Only if overrideClick is true. @Output() onClick?: EventEmitter<void>; // Will notify when the file is clicked. Only if overrideClick is true.
@ViewChild('nameForm') formElement: ElementRef;
fileName: string; fileName: string;
fileIcon: string; fileIcon: string;
fileExtension: string; fileExtension: string;
@ -47,12 +51,14 @@ export class CoreLocalFileComponent implements OnInit {
editMode: boolean; editMode: boolean;
relativePath: string; relativePath: string;
constructor(private mimeUtils: CoreMimetypeUtilsProvider, constructor(protected mimeUtils: CoreMimetypeUtilsProvider,
private utils: CoreUtilsProvider, protected utils: CoreUtilsProvider,
private textUtils: CoreTextUtilsProvider, protected textUtils: CoreTextUtilsProvider,
private fileProvider: CoreFileProvider, protected fileProvider: CoreFileProvider,
private domUtils: CoreDomUtilsProvider, protected domUtils: CoreDomUtilsProvider,
private timeUtils: CoreTimeUtilsProvider) { protected timeUtils: CoreTimeUtilsProvider,
protected sitesProvider: CoreSitesProvider,
protected eventsProvider: CoreEventsProvider) {
this.onDelete = new EventEmitter(); this.onDelete = new EventEmitter();
this.onRename = new EventEmitter(); this.onRename = new EventEmitter();
this.onClick = new EventEmitter(); this.onClick = new EventEmitter();
@ -137,6 +143,7 @@ export class CoreLocalFileComponent implements OnInit {
if (newName == this.file.name) { if (newName == this.file.name) {
// Name hasn't changed, stop. // Name hasn't changed, stop.
this.editMode = false; this.editMode = false;
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
return; return;
} }
@ -152,6 +159,10 @@ export class CoreLocalFileComponent implements OnInit {
}).catch(() => { }).catch(() => {
// File doesn't exist, move it. // File doesn't exist, move it.
return this.fileProvider.moveFile(this.relativePath, newPath).then((fileEntry) => { return this.fileProvider.moveFile(this.relativePath, newPath).then((fileEntry) => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false,
this.sitesProvider.getCurrentSiteId());
this.editMode = false; this.editMode = false;
this.file = fileEntry; this.file = fileEntry;
this.loadFileBasicData(); this.loadFileBasicData();

View File

@ -1,4 +1,4 @@
<form> <form #messageForm>
<textarea class="core-send-message-input" [core-auto-focus]="showKeyboard" [placeholder]="placeholder" rows="1" core-auto-rows [(ngModel)]="message" name="message" (onResize)="textareaResized()" (keydown.enter)="enterClicked($event)" (keydown.control.enter)="enterClicked($event, 'control')" (keydown.meta.enter)="enterClicked($event, 'meta')" aria-multiline="true"></textarea> <textarea class="core-send-message-input" [core-auto-focus]="showKeyboard" [placeholder]="placeholder" rows="1" core-auto-rows [(ngModel)]="message" name="message" (onResize)="textareaResized()" (keydown.enter)="enterClicked($event)" (keydown.control.enter)="enterClicked($event, 'control')" (keydown.meta.enter)="enterClicked($event, 'meta')" aria-multiline="true"></textarea>
<ion-buttons end> <ion-buttons end>
<button ion-button icon-only clear="true" type="submit" [disabled]="!message || sendDisabled" [attr.aria-label]="'core.send' | translate" [core-suppress-events] (onClick)="submitForm($event)"> <button ion-button icon-only clear="true" type="submit" [disabled]="!message || sendDisabled" [attr.aria-label]="'core.send' | translate" [core-suppress-events] (onClick)="submitForm($event)">

View File

@ -12,13 +12,14 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; import { Component, Input, Output, EventEmitter, OnInit, ViewChild, ElementRef } from '@angular/core';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreConfigProvider } from '@providers/config'; import { CoreConfigProvider } from '@providers/config';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
/** /**
@ -43,10 +44,17 @@ export class CoreSendMessageFormComponent implements OnInit {
@Output() onSubmit: EventEmitter<string>; // Send data when submitting the message form. @Output() onSubmit: EventEmitter<string>; // Send data when submitting the message form.
@Output() onResize: EventEmitter<void>; // Emit when resizing the textarea. @Output() onResize: EventEmitter<void>; // Emit when resizing the textarea.
@ViewChild('messageForm') formElement: ElementRef;
protected sendOnEnter: boolean; protected sendOnEnter: boolean;
constructor(private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider, configProvider: CoreConfigProvider, constructor(protected utils: CoreUtilsProvider,
eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider) { protected textUtils: CoreTextUtilsProvider,
configProvider: CoreConfigProvider,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider,
protected appProvider: CoreAppProvider,
protected domUtils: CoreDomUtilsProvider) {
this.onSubmit = new EventEmitter(); this.onSubmit = new EventEmitter();
this.onResize = new EventEmitter(); this.onResize = new EventEmitter();
@ -82,6 +90,8 @@ export class CoreSendMessageFormComponent implements OnInit {
this.message = ''; // Reset the form. this.message = ''; // Reset the form.
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false, this.sitesProvider.getCurrentSiteId());
value = this.textUtils.replaceNewLines(value, '<br>'); value = this.textUtils.replaceNewLines(value, '<br>');
this.onSubmit.emit(value); this.onSubmit.emit(value);
} }

View File

@ -9,7 +9,7 @@
</ion-navbar> </ion-navbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<form name="itemEdit" (ngSubmit)="addComment($event)"> <form name="itemEdit" (ngSubmit)="addComment($event)" #commentForm>
<ion-item> <ion-item>
<ion-textarea placeholder="{{ 'core.comments.addcomment' | translate }}" rows="5" [(ngModel)]="content" name="content" required="required"></ion-textarea> <ion-textarea placeholder="{{ 'core.comments.addcomment' | translate }}" rows="5" [(ngModel)]="content" name="content" required="required"></ion-textarea>
</ion-item> </ion-item>

View File

@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component } from '@angular/core'; import { Component, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, ViewController, NavParams } from 'ionic-angular'; import { IonicPage, ViewController, NavParams } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreCommentsProvider } from '../../providers/comments'; import { CoreCommentsProvider } from '../../providers/comments';
@ -27,6 +29,8 @@ import { CoreCommentsProvider } from '../../providers/comments';
templateUrl: 'add.html', templateUrl: 'add.html',
}) })
export class CoreCommentsAddPage { export class CoreCommentsAddPage {
@ViewChild('commentForm') formElement: ElementRef;
protected contextLevel: string; protected contextLevel: string;
protected instanceId: number; protected instanceId: number;
protected componentName: string; protected componentName: string;
@ -36,8 +40,13 @@ export class CoreCommentsAddPage {
content = ''; content = '';
processing = false; processing = false;
constructor(params: NavParams, private viewCtrl: ViewController, private appProvider: CoreAppProvider, constructor(params: NavParams,
private domUtils: CoreDomUtilsProvider, private commentsProvider: CoreCommentsProvider) { protected viewCtrl: ViewController,
protected appProvider: CoreAppProvider,
protected domUtils: CoreDomUtilsProvider,
protected commentsProvider: CoreCommentsProvider,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider) {
this.contextLevel = params.get('contextLevel'); this.contextLevel = params.get('contextLevel');
this.instanceId = params.get('instanceId'); this.instanceId = params.get('instanceId');
this.componentName = params.get('componentName'); this.componentName = params.get('componentName');
@ -61,6 +70,10 @@ export class CoreCommentsAddPage {
this.processing = true; this.processing = true;
this.commentsProvider.addComment(this.content, this.contextLevel, this.instanceId, this.componentName, this.itemId, this.commentsProvider.addComment(this.content, this.contextLevel, this.instanceId, this.componentName, this.itemId,
this.area).then((commentsResponse) => { this.area).then((commentsResponse) => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, !!commentsResponse,
this.sitesProvider.getCurrentSiteId());
this.viewCtrl.dismiss({comments: commentsResponse}).finally(() => { this.viewCtrl.dismiss({comments: commentsResponse}).finally(() => {
this.domUtils.showToast(commentsResponse ? 'core.comments.eventcommentcreated' : 'core.datastoredoffline', true, this.domUtils.showToast(commentsResponse ? 'core.comments.eventcommentcreated' : 'core.datastoredoffline', true,
3000); 3000);
@ -77,6 +90,7 @@ export class CoreCommentsAddPage {
* Close modal. * Close modal.
*/ */
closeModal(): void { closeModal(): void {
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
this.viewCtrl.dismiss(); this.viewCtrl.dismiss();
} }
} }

View File

@ -54,7 +54,7 @@ export class CoreCommentsProvider {
// Convenience function to store a comment to be synchronized later. // Convenience function to store a comment to be synchronized later.
const storeOffline = (): Promise<any> => { const storeOffline = (): Promise<any> => {
return this.commentsOffline.saveComment(content, contextLevel, instanceId, component, itemId, area, siteId).then(() => { return this.commentsOffline.saveComment(content, contextLevel, instanceId, component, itemId, area, siteId).then(() => {
return Promise.resolve(false); return false;
}); });
}; };

View File

@ -10,7 +10,7 @@
</ion-navbar> </ion-navbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<form ion-list #f="ngForm" (ngSubmit)="submitPassword($event, f.value.password)"> <form ion-list #f="ngForm" (ngSubmit)="submitPassword($event, f.value.password)" #enrolPasswordForm>
<ion-item> <ion-item>
<core-show-password item-content [name]="'password'"> <core-show-password item-content [name]="'password'">
<ion-input text-wrap class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.courses.password' | translate }}" ngModel [core-auto-focus] [clearOnEdit]="false"></ion-input> <ion-input text-wrap class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.courses.password' | translate }}" ngModel [core-auto-focus] [clearOnEdit]="false"></ion-input>

View File

@ -12,8 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component } from '@angular/core'; import { Component, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, ViewController } from 'ionic-angular'; import { IonicPage, ViewController } from 'ionic-angular';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
/** /**
* Page that displays a form to enter a password to self enrol in a course. * Page that displays a form to enter a password to self enrol in a course.
@ -24,12 +27,19 @@ import { IonicPage, ViewController } from 'ionic-angular';
templateUrl: 'self-enrol-password.html', templateUrl: 'self-enrol-password.html',
}) })
export class CoreCoursesSelfEnrolPasswordPage { export class CoreCoursesSelfEnrolPasswordPage {
constructor(private viewCtrl: ViewController) { }
@ViewChild('enrolPasswordForm') formElement: ElementRef;
constructor(protected viewCtrl: ViewController,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider,
protected domUtils: CoreDomUtilsProvider) { }
/** /**
* Close help modal. * Close help modal.
*/ */
close(): void { close(): void {
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
this.viewCtrl.dismiss(); this.viewCtrl.dismiss();
} }
@ -43,6 +53,8 @@ export class CoreCoursesSelfEnrolPasswordPage {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false, this.sitesProvider.getCurrentSiteId());
this.viewCtrl.dismiss(password); this.viewCtrl.dismiss(password);
} }
} }

View File

@ -0,0 +1,41 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreEditorRichTextEditorComponent } from './rich-text-editor/rich-text-editor';
import { CoreComponentsModule } from '@components/components.module';
@NgModule({
declarations: [
CoreEditorRichTextEditorComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
],
providers: [
],
exports: [
CoreEditorRichTextEditorComponent
],
entryComponents: [
CoreEditorRichTextEditorComponent
]
})
export class CoreEditorComponentsModule {}

View File

@ -1,8 +1,15 @@
<div class="core-rte-editor-container" (click)="focusRTE()" [class.toolbar-hidden]="toolbarHidden">
<div [hidden]="!rteEnabled" #editor contenteditable="true" class="core-rte-editor" tappable (focus)="showToolbar()" (longPress)="showToolbar()" (blur)="hideToolbar($event)" [attr.data-placeholder-text]="placeholder" role="textbox"> <div [hidden]="!rteEnabled" #editor contenteditable="true" class="core-rte-editor" tappable (focus)="showToolbar()" (longPress)="showToolbar()" (blur)="hideToolbar($event)" [attr.data-placeholder-text]="placeholder" role="textbox">
</div> </div>
<ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" [placeholder]="placeholder" [attr.name]="name" ngControl="control" (ionChange)="onChange($event)" (focus)="showToolbar()" (longPress)="showToolbar()" (blur)="hideToolbar($event)" role="textbox"></ion-textarea> <ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" [placeholder]="placeholder" [attr.name]="name" ngControl="control" (ionChange)="onChange($event)" (focus)="showToolbar()" (longPress)="showToolbar()" (blur)="hideToolbar($event)" role="textbox"></ion-textarea>
<div class="core-rte-info-message" *ngIf="infoMessage">
<ion-icon name="information-circle"></ion-icon>
{{ infoMessage | translate }}
</div>
</div>
<div #toolbar class="core-rte-toolbar" [class.toolbar-hidden]="toolbarHidden"> <div #toolbar class="core-rte-toolbar" [class.toolbar-hidden]="toolbarHidden">
<button *ngIf="toolbarArrows" class="toolbar-arrow" [class.toolbar-arrow-hidden]="toolbarPrevHidden" (click)="toolbarPrev($event)" (mousedown)="stopBubble($event)"> <button *ngIf="toolbarArrows" class="toolbar-arrow" [class.toolbar-arrow-hidden]="toolbarPrevHidden" (click)="toolbarPrev($event)" (mousedown)="stopBubble($event)">
<ion-icon name="arrow-back" md="ios-arrow-back"></ion-icon> <ion-icon name="arrow-back" md="ios-arrow-back"></ion-icon>

View File

@ -10,12 +10,35 @@ ion-app.app-root core-rich-text-editor {
background-color: $gray-darker; background-color: $gray-darker;
} }
.core-rte-editor-container {
max-height: calc(100% - 46px);
display: flex;
flex-direction: column;
flex-grow: 1;
&.toolbar-hidden {
max-height: 100%;
}
.core-rte-info-message {
padding: 5px;
border-top: 1px solid $info;
background: white;
flex-shrink: 1;
font-size: 1.4rem;
.icon {
color: $info;
}
}
}
.core-rte-editor, .core-textarea { .core-rte-editor, .core-textarea {
padding: 2px; padding: 2px;
margin: 2px; margin: 2px;
width: 100%; width: 100%;
resize: none; resize: none;
background-color: $white; background-color: $white;
flex-grow: 1;
@include darkmode() { @include darkmode() {
background-color: $gray-darker; background-color: $gray-darker;
color: $white; color: $white;
@ -147,7 +170,6 @@ ion-app.app-root core-rich-text-editor {
border: none; border: none;
} }
} }
} }
body.keyboard-is-open ion-app.app-root core-rich-text-editor { body.keyboard-is-open ion-app.app-root core-rich-text-editor {

View File

@ -21,28 +21,23 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreEditorOfflineProvider } from '../../providers/editor-offline';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
/** /**
* Directive to display a rich text editor if enabled. * Component to display a rich text editor if enabled.
* *
* If enabled, this directive will show a rich text editor. Otherwise it'll show a regular textarea. * If enabled, this component will show a rich text editor. Otherwise it'll show a regular textarea.
*
* This directive requires an OBJECT model. The text written in the editor or textarea will be stored inside
* a "text" property in that object. This is to ensure 2-way data-binding, since using a string as a model
* could be easily broken.
* *
* Example: * Example:
* <core-rich-text-editor item-content [control]="control" [placeholder]="field.name"></core-rich-text-editor> * <core-rich-text-editor item-content [control]="control" [placeholder]="field.name"></core-rich-text-editor>
*
* In the example above, the text written in the editor will be stored in newpost.text.
*/ */
@Component({ @Component({
selector: 'core-rich-text-editor', selector: 'core-rich-text-editor',
templateUrl: 'core-rich-text-editor.html' templateUrl: 'core-editor-rich-text-editor.html'
}) })
export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy { export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDestroy {
// Based on: https://github.com/judgewest2000/Ionic3RichText/ // Based on: https://github.com/judgewest2000/Ionic3RichText/
// @todo: Anchor button, fullscreen... // @todo: Anchor button, fullscreen...
// @todo: Textarea height is not being updated when editor is resized. Height is calculated if any css is changed. // @todo: Textarea height is not being updated when editor is resized. Height is calculated if any css is changed.
@ -52,11 +47,19 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
@Input() name = 'core-rich-text-editor'; // Name to set to the textarea. @Input() name = 'core-rich-text-editor'; // Name to set to the textarea.
@Input() component?: string; // The component to link the files to. @Input() component?: string; // The component to link the files to.
@Input() componentId?: number; // An ID to use in conjunction with the component. @Input() componentId?: number; // An ID to use in conjunction with the component.
@Input() autoSave?: boolean | string; // Whether to auto-save the contents in a draft. Defaults to true.
@Input() contextLevel?: string; // The context level of the text.
@Input() contextInstanceId?: number; // The instance ID related to the context.
@Input() elementId?: string; // An ID to set to the element.
@Input() draftExtraParams: {[name: string]: any}; // Extra params to identify the draft.
@Output() contentChanged: EventEmitter<string>; @Output() contentChanged: EventEmitter<string>;
@ViewChild('editor') editor: ElementRef; // WYSIWYG editor. @ViewChild('editor') editor: ElementRef; // WYSIWYG editor.
@ViewChild('textarea') textarea: TextInput; // Textarea editor. @ViewChild('textarea') textarea: TextInput; // Textarea editor.
protected DRAFT_AUTOSAVE_FREQUENCY = 30000;
protected RESTORE_MESSAGE_CLEAR_TIME = 6000;
protected SAVE_MESSAGE_CLEAR_TIME = 2000;
protected element: HTMLDivElement; protected element: HTMLDivElement;
protected editorElement: HTMLDivElement; protected editorElement: HTMLDivElement;
protected kbHeight = 0; // Last known keyboard height. protected kbHeight = 0; // Last known keyboard height.
@ -64,7 +67,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
protected valueChangeSubscription: Subscription; protected valueChangeSubscription: Subscription;
protected keyboardObs: any; protected keyboardObs: any;
protected initHeightInterval; protected resetObs: any;
protected initHeightInterval: NodeJS.Timer;
rteEnabled = false; rteEnabled = false;
editorSupported = true; editorSupported = true;
@ -90,16 +94,31 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
ul: 'false', ul: 'false',
ol: 'false', ol: 'false',
}; };
infoMessage: string;
protected isCurrentView = true; protected isCurrentView = true;
protected toolbarButtonWidth = 40; protected toolbarButtonWidth = 40;
protected toolbarArrowWidth = 28; protected toolbarArrowWidth = 28;
protected pageInstance: string;
protected autoSaveInterval: NodeJS.Timer;
protected hideMessageTimeout: NodeJS.Timer;
protected lastDraft = '';
protected draftWasRestored = false;
protected originalContent: string;
constructor(private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, constructor(
private sitesProvider: CoreSitesProvider, private filepoolProvider: CoreFilepoolProvider, protected domUtils: CoreDomUtilsProvider,
@Optional() private content: Content, elementRef: ElementRef, private events: CoreEventsProvider, protected urlUtils: CoreUrlUtilsProvider,
private utils: CoreUtilsProvider, private platform: Platform) { protected sitesProvider: CoreSitesProvider,
protected filepoolProvider: CoreFilepoolProvider,
@Optional() protected content: Content,
elementRef: ElementRef,
protected events: CoreEventsProvider,
protected utils: CoreUtilsProvider,
protected platform: Platform,
protected editorOffline: CoreEditorOfflineProvider) {
this.contentChanged = new EventEmitter<string>(); this.contentChanged = new EventEmitter<string>();
this.element = elementRef.nativeElement as HTMLDivElement; this.element = elementRef.nativeElement as HTMLDivElement;
this.pageInstance = 'app_' + Date.now(); // Generate a "unique" ID based on timestamp.
} }
/** /**
@ -115,6 +134,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
// Setup the editor. // Setup the editor.
this.editorElement = this.editor.nativeElement as HTMLDivElement; this.editorElement = this.editor.nativeElement as HTMLDivElement;
this.setContent(this.control.value); this.setContent(this.control.value);
this.originalContent = this.control.value;
this.lastDraft = this.control.value;
this.editorElement.onchange = this.onChange.bind(this); this.editorElement.onchange = this.onChange.bind(this);
this.editorElement.onkeyup = this.onChange.bind(this); this.editorElement.onkeyup = this.onChange.bind(this);
this.editorElement.onpaste = this.onChange.bind(this); this.editorElement.onpaste = this.onChange.bind(this);
@ -123,7 +144,20 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
// Listen for changes on the control to update the editor (if it is updated from outside of this component). // Listen for changes on the control to update the editor (if it is updated from outside of this component).
this.valueChangeSubscription = this.control.valueChanges.subscribe((param) => { this.valueChangeSubscription = this.control.valueChanges.subscribe((param) => {
if (!this.draftWasRestored || this.originalContent != param) {
// Apply the new content.
this.setContent(param); this.setContent(param);
this.originalContent = param;
this.infoMessage = null;
// Save a draft so the original content is saved.
this.lastDraft = param;
this.editorOffline.saveDraft(this.contextLevel, this.contextInstanceId, this.elementId,
this.draftExtraParams, this.pageInstance, param, param);
} else {
// A draft was restored and the content hasn't changed in the site. Use the draft value instead of this one.
this.control.setValue(this.lastDraft, {emitEvent: false});
}
}); });
// Use paragraph on enter. // Use paragraph on enter.
@ -148,6 +182,20 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
}); });
this.updateToolbarButtons(); this.updateToolbarButtons();
if (this.elementId) {
// Prepend elementId with 'id_' like in web. Don't use a setter for this because the value shouldn't change.
this.elementId = 'id_' + this.elementId;
this.element.setAttribute('id', this.elementId);
}
if (this.shouldAutoSaveDrafts()) {
this.restoreDraft();
this.autoSaveDrafts();
this.deleteDraftOnSubmitOrCancel();
}
} }
/** /**
@ -196,7 +244,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
} }
if (height > this.minHeight) { if (height > this.minHeight) {
this.element.style.height = this.domUtils.formatPixelsSize(height); this.element.style.height = this.domUtils.formatPixelsSize(height - 1);
} else { } else {
this.element.style.height = ''; this.element.style.height = '';
} }
@ -549,10 +597,23 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
} }
} }
/**
* Focus editor when click the area.
*
* @param e Event
*/
focusRTE(e?: Event): void {
if (this.rteEnabled) {
this.editorElement.focus();
} else {
this.textarea.setFocus();
}
}
/** /**
* Hide the toolbar in phone mode. * Hide the toolbar in phone mode.
*/ */
hideToolbar($event: any): void { hideToolbar($event: Event): void {
this.stopBubble($event); this.stopBubble($event);
if (this.isPhone) { if (this.isPhone) {
@ -673,6 +734,121 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
} }
} }
/**
* Check if should auto save drafts.
*
* @return {boolean} Whether it should auto save drafts.
*/
protected shouldAutoSaveDrafts(): boolean {
return !!this.sitesProvider.getCurrentSite() &&
(typeof this.autoSave == 'undefined' || this.utils.isTrueOrOne(this.autoSave)) &&
typeof this.contextLevel != 'undefined' &&
typeof this.contextInstanceId != 'undefined' &&
typeof this.elementId != 'undefined';
}
/**
* Restore a draft if there is any.
*
* @return Promise resolved when done.
*/
protected async restoreDraft(): Promise<void> {
try {
const entry = await this.editorOffline.resumeDraft(this.contextLevel, this.contextInstanceId, this.elementId,
this.draftExtraParams, this.pageInstance, this.originalContent);
if (typeof entry == 'undefined') {
// No draft found.
return;
}
let draftText = entry.drafttext;
// Revert untouched editor contents to an empty string.
if (draftText == '<p></p>' || draftText == '<p><br></p>' || draftText == '<br>' ||
draftText == '<p>&nbsp;</p>' || draftText == '<p><br>&nbsp;</p>') {
draftText = '';
}
if (draftText !== '' && draftText != this.control.value) {
// Restore the draft.
this.control.setValue(draftText, {emitEvent: false});
this.setContent(draftText);
this.lastDraft = draftText;
this.draftWasRestored = true;
this.originalContent = entry.originalcontent;
if (entry.drafttext != entry.originalcontent) {
// Notify the user.
this.showMessage('core.editor.textrecovered', this.RESTORE_MESSAGE_CLEAR_TIME);
}
}
} catch (error) {
// Ignore errors, shouldn't happen.
}
}
/**
* Automatically save drafts every certain time.
*/
protected autoSaveDrafts(): void {
this.autoSaveInterval = setInterval(async () => {
const newText = this.control.value;
if (this.lastDraft == newText) {
// Text hasn't changed, nothing to save.
return;
}
try {
await this.editorOffline.saveDraft(this.contextLevel, this.contextInstanceId, this.elementId,
this.draftExtraParams, this.pageInstance, newText, this.originalContent);
// Draft saved, notify the user.
this.lastDraft = newText;
this.showMessage('core.editor.autosavesucceeded', this.SAVE_MESSAGE_CLEAR_TIME);
} catch (error) {
// Error saving draft.
}
}, this.DRAFT_AUTOSAVE_FREQUENCY);
}
/**
* Delete the draft when the form is submitted or cancelled.
*/
protected deleteDraftOnSubmitOrCancel(): void {
this.resetObs = this.events.on(CoreEventsProvider.FORM_ACTION, async (data) => {
const form = this.element.closest('form');
if (data.form && form && data.form == form) {
try {
await this.editorOffline.deleteDraft(this.contextLevel, this.contextInstanceId, this.elementId,
this.draftExtraParams);
} catch (error) {
// Error deleting draft. Shouldn't happen.
}
}
}, this.sitesProvider.getCurrentSiteId());
}
/**
* Show a message.
*
* @param message Identifier of the message to display.
* @param timeout Number of milliseconds when to remove the message.
*/
protected showMessage(message: string, timeout: number): void {
clearTimeout(this.hideMessageTimeout);
this.infoMessage = message;
this.hideMessageTimeout = setTimeout(() => {
this.hideMessageTimeout = null;
this.infoMessage = null;
}, timeout);
}
/** /**
* User entered the page that contains the component. * User entered the page that contains the component.
*/ */
@ -698,5 +874,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
document.removeEventListener('selectionchange', this.updateToolbarStyles); document.removeEventListener('selectionchange', this.updateToolbarStyles);
clearInterval(this.initHeightInterval); clearInterval(this.initHeightInterval);
this.keyboardObs && this.keyboardObs.off(); this.keyboardObs && this.keyboardObs.off();
clearInterval(this.autoSaveInterval);
clearTimeout(this.hideMessageTimeout);
this.resetObs && this.resetObs.off();
} }
} }

View File

@ -0,0 +1,38 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CoreEditorComponentsModule } from './components/components.module';
import { CoreEditorOfflineProvider } from './providers/editor-offline';
// List of providers (without handlers).
export const CORE_GRADES_PROVIDERS: any[] = [
CoreEditorOfflineProvider,
];
@NgModule({
declarations: [
],
imports: [
CoreEditorComponentsModule,
],
providers: [
CoreEditorOfflineProvider,
],
})
export class CoreEditorModule {
constructor(editorOffline: CoreEditorOfflineProvider) {
// Inject the helper even if it isn't used here it's instantiated.
}
}

View File

@ -0,0 +1,4 @@
{
"autosavesucceeded": "Draft saved.",
"textrecovered": "A draft version of this text was automatically restored."
}

View File

@ -0,0 +1,277 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUtilsProvider } from '@providers/utils/utils';
/**
* Service with features regarding rich text editor in offline.
*/
@Injectable()
export class CoreEditorOfflineProvider {
protected DRAFT_TABLE = 'editor_draft';
protected logger;
protected siteSchema: CoreSiteSchema = {
name: 'CoreEditorProvider',
version: 1,
tables: [
{
name: this.DRAFT_TABLE,
columns: [
{
name: 'contextlevel',
type: 'TEXT',
},
{
name: 'contextinstanceid',
type: 'INTEGER',
},
{
name: 'elementid',
type: 'TEXT',
},
{
name: 'extraparams', // Moodle web uses a page hash built with URL. App will use some params stringified.
type: 'TEXT',
},
{
name: 'drafttext',
type: 'TEXT',
notNull: true,
},
{
name: 'pageinstance',
type: 'TEXT',
notNull: true,
},
{
name: 'timecreated',
type: 'INTEGER',
notNull: true,
},
{
name: 'timemodified',
type: 'INTEGER',
notNull: true,
},
{
name: 'originalcontent',
type: 'TEXT',
},
],
primaryKeys: ['contextlevel', 'contextinstanceid', 'elementid', 'extraparams'],
},
],
};
constructor(
logger: CoreLoggerProvider,
protected sitesProvider: CoreSitesProvider,
protected textUtils: CoreTextUtilsProvider,
protected utils: CoreUtilsProvider) {
this.logger = logger.getInstance('CoreEditorProvider');
this.sitesProvider.registerSiteSchema(this.siteSchema);
}
/**
* Delete a draft from DB.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteDraft(contextLevel: string, contextInstanceId: number, elementId: string, extraParams: {[name: string]: any},
siteId?: string): Promise<void> {
try {
const db = await this.sitesProvider.getSiteDb(siteId);
const params = this.fixDraftPrimaryData(contextLevel, contextInstanceId, elementId, extraParams);
return db.deleteRecords(this.DRAFT_TABLE, params);
} catch (error) {
// Ignore errors, probably no draft stored.
}
}
/**
* Return an object with the draft primary data converted to the right format.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @return Object with the fixed primary data.
*/
protected fixDraftPrimaryData(contextLevel: string, contextInstanceId: number, elementId: string,
extraParams: {[name: string]: any}): CoreEditorDraftPrimaryData {
return {
contextlevel: contextLevel,
contextinstanceid: contextInstanceId,
elementid: elementId,
extraparams: this.utils.sortAndStringify(extraParams || {}),
};
}
/**
* Get a draft from DB.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the draft data. Undefined if no draft stored.
*/
async getDraft(contextLevel: string, contextInstanceId: number, elementId: string, extraParams: {[name: string]: any},
siteId?: string): Promise<CoreEditorDraft> {
const db = await this.sitesProvider.getSiteDb(siteId);
const params = this.fixDraftPrimaryData(contextLevel, contextInstanceId, elementId, extraParams);
return db.getRecord(this.DRAFT_TABLE, params);
}
/**
* Get draft to resume it.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @param pageInstance Unique identifier to prevent storing data from several sources at the same time.
* @param originalContent Original content of the editor.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the draft data. Undefined if no draft stored.
*/
async resumeDraft(contextLevel: string, contextInstanceId: number, elementId: string, extraParams: {[name: string]: any},
pageInstance: string, originalContent?: string, siteId?: string): Promise<CoreEditorDraft> {
try {
// Check if there is a draft stored.
const entry = await this.getDraft(contextLevel, contextInstanceId, elementId, extraParams, siteId);
// There is a draft stored. Update its page instance.
try {
const db = await this.sitesProvider.getSiteDb(siteId);
entry.pageinstance = pageInstance;
entry.timemodified = Date.now();
if (originalContent && entry.originalcontent != originalContent) {
entry.originalcontent = originalContent;
entry.drafttext = ''; // "Discard" the draft.
}
await db.insertRecord(this.DRAFT_TABLE, entry);
} catch (error) {
// Ignore errors saving the draft. It shouldn't happen.
}
return entry;
} catch (error) {
// No draft stored. Store an empty draft to save the pageinstance.
await this.saveDraft(contextLevel, contextInstanceId, elementId, extraParams, pageInstance, '', originalContent,
siteId);
}
}
/**
* Save a draft in DB.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @param pageInstance Unique identifier to prevent storing data from several sources at the same time.
* @param draftText The text to store.
* @param originalContent Original content of the editor.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async saveDraft(contextLevel: string, contextInstanceId: number, elementId: string, extraParams: {[name: string]: any},
pageInstance: string, draftText: string, originalContent?: string, siteId?: string): Promise<void> {
let timecreated = Date.now();
let entry: CoreEditorDraft;
// Check if there is a draft already stored.
try {
entry = await this.getDraft(contextLevel, contextInstanceId, elementId, extraParams, siteId);
timecreated = entry.timecreated;
} catch (error) {
// No draft already stored.
}
if (entry) {
if (entry.pageinstance != pageInstance) {
this.logger.warning(`Discarding draft because of pageinstance. Context '${contextLevel}' '${contextInstanceId}', ` +
`element '${elementId}'`);
throw null;
}
if (!originalContent) {
// Original content not set, use the one in the entry.
originalContent = entry.originalcontent;
}
}
const db = await this.sitesProvider.getSiteDb(siteId);
const data: CoreEditorDraft = this.fixDraftPrimaryData(contextLevel, contextInstanceId, elementId, extraParams);
data.drafttext = (draftText || '').trim();
data.pageinstance = pageInstance;
data.timecreated = timecreated;
data.timemodified = Date.now();
if (originalContent) {
data.originalcontent = originalContent;
}
await db.insertRecord(this.DRAFT_TABLE, data);
}
}
/**
* Primary data to identify a stored draft.
*/
type CoreEditorDraftPrimaryData = {
contextlevel: string; // Context level.
contextinstanceid: number; // The instance ID related to the context.
elementid: string; // Element ID.
extraparams: string; // Extra params stringified.
};
/**
* Draft data stored.
*/
type CoreEditorDraft = CoreEditorDraftPrimaryData & {
drafttext?: string; // Draft text stored.
pageinstance?: string; // Unique identifier to prevent storing data from several sources at the same time.
timecreated?: number; // Time created.
timemodified?: number; // Time modified.
originalcontent?: string; // Original content of the editor.
};

View File

@ -23,7 +23,7 @@
<p *ngIf="siteName" padding class="item-heading core-sitename"><core-format-text [text]="siteName" [filter]="false"></core-format-text></p> <p *ngIf="siteName" padding class="item-heading core-sitename"><core-format-text [text]="siteName" [filter]="false"></core-format-text></p>
<p *ngIf="siteName" class="core-siteurl">{{siteUrl}}</p> <p *ngIf="siteName" class="core-siteurl">{{siteUrl}}</p>
</div> </div>
<form ion-list [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form"> <form ion-list [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #credentialsForm>
<ion-item *ngIf="siteChecked && !isBrowserSSO"> <ion-item *ngIf="siteChecked && !isBrowserSSO">
<ion-input type="text" name="username" placeholder="{{ 'core.login.username' | translate }}" formControlName="username" autocapitalize="none" autocorrect="off"></ion-input> <ion-input type="text" name="username" placeholder="{{ 'core.login.username' | translate }}" formControlName="username" autocapitalize="none" autocorrect="off"></ion-input>
</ion-item> </ion-item>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component } from '@angular/core'; import { Component, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
@ -32,6 +32,9 @@ import { CoreConfigConstants } from '../../../../configconstants';
templateUrl: 'credentials.html', templateUrl: 'credentials.html',
}) })
export class CoreLoginCredentialsPage { export class CoreLoginCredentialsPage {
@ViewChild('credentialsForm') formElement: ElementRef;
credForm: FormGroup; credForm: FormGroup;
siteUrl: string; siteUrl: string;
siteChecked = false; siteChecked = false;
@ -242,6 +245,8 @@ export class CoreLoginCredentialsPage {
} }
}).finally(() => { }).finally(() => {
modal.dismiss(); modal.dismiss();
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, true);
}); });
} }

View File

@ -17,7 +17,7 @@
<core-loading [hideUntil]="settingsLoaded" *ngIf="!isMinor"> <core-loading [hideUntil]="settingsLoaded" *ngIf="!isMinor">
<!-- Age verification. --> <!-- Age verification. -->
<form ion-list *ngIf="settingsLoaded && settings && ageDigitalConsentVerification" [formGroup]="ageVerificationForm" (ngSubmit)="verifyAge($event)"> <form ion-list *ngIf="settingsLoaded && settings && ageDigitalConsentVerification" [formGroup]="ageVerificationForm" (ngSubmit)="verifyAge($event)" #ageForm>
<ion-item-divider text-wrap> <ion-item-divider text-wrap>
<p class="item-heading">{{ 'core.agelocationverification' | translate }}</p> <p class="item-heading">{{ 'core.agelocationverification' | translate }}</p>
</ion-item-divider> </ion-item-divider>
@ -47,7 +47,7 @@
</form> </form>
<!-- Signup form. --> <!-- Signup form. -->
<form ion-list *ngIf="settingsLoaded && settings && !ageDigitalConsentVerification" [formGroup]="signupForm" (ngSubmit)="create($event)"> <form ion-list *ngIf="settingsLoaded && settings && !ageDigitalConsentVerification" [formGroup]="signupForm" (ngSubmit)="create($event)" #signupFormEl>
<ion-item text-wrap text-center> <ion-item text-wrap text-center>
<!-- If no sitename show big siteurl. --> <!-- If no sitename show big siteurl. -->
<p *ngIf="!siteName" padding class="item-heading">{{siteUrl}}</p> <p *ngIf="!siteName" padding class="item-heading">{{siteUrl}}</p>

View File

@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, ViewChild } from '@angular/core'; import { Component, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavController, NavParams, Content } from 'ionic-angular'; import { IonicPage, NavController, NavParams, Content } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
@ -35,6 +36,8 @@ import { CoreConfigConstants } from '../../../../configconstants';
}) })
export class CoreLoginEmailSignupPage { export class CoreLoginEmailSignupPage {
@ViewChild(Content) content: Content; @ViewChild(Content) content: Content;
@ViewChild('ageForm') ageFormElement: ElementRef;
@ViewChild('signupFormEl') signupFormElement: ElementRef;
signupForm: FormGroup; signupForm: FormGroup;
siteUrl: string; siteUrl: string;
@ -66,10 +69,18 @@ export class CoreLoginEmailSignupPage {
policyErrors: any; policyErrors: any;
namefieldsErrors: any; namefieldsErrors: any;
constructor(private navCtrl: NavController, navParams: NavParams, private fb: FormBuilder, private wsProvider: CoreWSProvider, constructor(protected navCtrl: NavController,
private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider, navParams: NavParams,
private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private utils: CoreUtilsProvider, protected fb: FormBuilder,
private textUtils: CoreTextUtilsProvider, private userProfileFieldDelegate: CoreUserProfileFieldDelegate) { protected wsProvider: CoreWSProvider,
protected sitesProvider: CoreSitesProvider,
protected loginHelper: CoreLoginHelperProvider,
protected domUtils: CoreDomUtilsProvider,
protected translate: TranslateService,
protected utils: CoreUtilsProvider,
protected textUtils: CoreTextUtilsProvider,
protected userProfileFieldDelegate: CoreUserProfileFieldDelegate,
protected eventsProvider: CoreEventsProvider) {
this.siteUrl = navParams.get('siteUrl'); this.siteUrl = navParams.get('siteUrl');
@ -265,6 +276,9 @@ export class CoreLoginEmailSignupPage {
return this.wsProvider.callAjax('auth_email_signup_user', params, { siteUrl: this.siteUrl }); return this.wsProvider.callAjax('auth_email_signup_user', params, { siteUrl: this.siteUrl });
}).then((result) => { }).then((result) => {
if (result.success) { if (result.success) {
this.domUtils.triggerFormSubmittedEvent(this.signupFormElement.nativeElement, true);
// Show alert and ho back. // Show alert and ho back.
const message = this.translate.instant('core.login.emailconfirmsent', { $a: params.email }); const message = this.translate.instant('core.login.emailconfirmsent', { $a: params.email });
this.domUtils.showAlert(this.translate.instant('core.success'), message); this.domUtils.showAlert(this.translate.instant('core.success'), message);
@ -334,6 +348,9 @@ export class CoreLoginEmailSignupPage {
params.age = parseInt(params.age, 10); // Use just the integer part. params.age = parseInt(params.age, 10); // Use just the integer part.
this.wsProvider.callAjax('core_auth_is_minor', params, {siteUrl: this.siteUrl}).then((result) => { this.wsProvider.callAjax('core_auth_is_minor', params, {siteUrl: this.siteUrl}).then((result) => {
this.domUtils.triggerFormSubmittedEvent(this.ageFormElement.nativeElement, true);
if (!result.status) { if (!result.status) {
if (this.countryControl.value) { if (this.countryControl.value) {
this.signUpCountryControl.setValue(this.countryControl.value); this.signUpCountryControl.setValue(this.countryControl.value);

View File

@ -10,7 +10,7 @@
</ion-item> </ion-item>
</ion-list> </ion-list>
<ion-card> <ion-card>
<form ion-list [formGroup]="myForm" (ngSubmit)="resetPassword($event)"> <form ion-list [formGroup]="myForm" (ngSubmit)="resetPassword($event)" #resetPasswordForm>
<ion-item-divider text-wrap> <ion-item-divider text-wrap>
{{ 'core.login.searchby' | translate }} {{ 'core.login.searchby' | translate }}
</ion-item-divider> </ion-item-divider>

View File

@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component } from '@angular/core'; import { Component, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreLoginHelperProvider } from '../../providers/helper'; import { CoreLoginHelperProvider } from '../../providers/helper';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@ -28,11 +30,20 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
templateUrl: 'forgotten-password.html', templateUrl: 'forgotten-password.html',
}) })
export class CoreLoginForgottenPasswordPage { export class CoreLoginForgottenPasswordPage {
@ViewChild('resetPasswordForm') formElement: ElementRef;
myForm: FormGroup; myForm: FormGroup;
siteUrl: string; siteUrl: string;
constructor(private navCtrl: NavController, navParams: NavParams, fb: FormBuilder, private translate: TranslateService, constructor(protected navCtrl: NavController,
private loginHelper: CoreLoginHelperProvider, private domUtils: CoreDomUtilsProvider) { navParams: NavParams,
fb: FormBuilder,
protected translate: TranslateService,
protected loginHelper: CoreLoginHelperProvider,
protected domUtils: CoreDomUtilsProvider,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider) {
this.siteUrl = navParams.get('siteUrl'); this.siteUrl = navParams.get('siteUrl');
this.myForm = fb.group({ this.myForm = fb.group({
@ -71,6 +82,8 @@ export class CoreLoginForgottenPasswordPage {
this.domUtils.showErrorModal(response.notice); this.domUtils.showErrorModal(response.notice);
} else { } else {
// Success. // Success.
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, true);
this.domUtils.showAlert(this.translate.instant('core.success'), response.notice); this.domUtils.showAlert(this.translate.instant('core.success'), response.notice);
this.navCtrl.pop(); this.navCtrl.pop();
} }

View File

@ -29,7 +29,7 @@
<ion-icon padding name="alert"></ion-icon> {{ 'core.login.reconnectdescription' | translate }} <ion-icon padding name="alert"></ion-icon> {{ 'core.login.reconnectdescription' | translate }}
</p> </p>
</div> </div>
<form ion-list *ngIf="!isOAuth" [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form"> <form ion-list *ngIf="!isOAuth" [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #reconnectForm>
<ion-item text-wrap class="core-username"> <ion-item text-wrap class="core-username">
<p>{{username}}</p> <p>{{username}}</p>
</ion-item> </ion-item>

View File

@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component } from '@angular/core'; import { Component, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreLoginHelperProvider } from '../../providers/helper'; import { CoreLoginHelperProvider } from '../../providers/helper';
@ -29,6 +30,9 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
templateUrl: 'reconnect.html', templateUrl: 'reconnect.html',
}) })
export class CoreLoginReconnectPage { export class CoreLoginReconnectPage {
@ViewChild('reconnectForm') formElement: ElementRef;
credForm: FormGroup; credForm: FormGroup;
siteUrl: string; siteUrl: string;
username: string; username: string;
@ -47,13 +51,14 @@ export class CoreLoginReconnectPage {
protected isLoggedOut: boolean; protected isLoggedOut: boolean;
protected siteId: string; protected siteId: string;
constructor(private navCtrl: NavController, constructor(protected navCtrl: NavController,
navParams: NavParams, navParams: NavParams,
fb: FormBuilder, fb: FormBuilder,
private appProvider: CoreAppProvider, protected appProvider: CoreAppProvider,
private sitesProvider: CoreSitesProvider, protected sitesProvider: CoreSitesProvider,
private loginHelper: CoreLoginHelperProvider, protected loginHelper: CoreLoginHelperProvider,
private domUtils: CoreDomUtilsProvider) { protected domUtils: CoreDomUtilsProvider,
protected eventsProvider: CoreEventsProvider) {
const currentSite = this.sitesProvider.getCurrentSite(); const currentSite = this.sitesProvider.getCurrentSite();
@ -175,6 +180,9 @@ export class CoreLoginReconnectPage {
// Start the authentication process. // Start the authentication process.
this.sitesProvider.getUserToken(siteUrl, username, password).then((data) => { this.sitesProvider.getUserToken(siteUrl, username, password).then((data) => {
return this.sitesProvider.updateSiteToken(this.infoSiteUrl, username, data.token, data.privateToken).then(() => { return this.sitesProvider.updateSiteToken(this.infoSiteUrl, username, data.token, data.privateToken).then(() => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, true);
// Update site info too because functions might have changed (e.g. unisntall local_mobile). // Update site info too because functions might have changed (e.g. unisntall local_mobile).
return this.sitesProvider.updateSiteInfoByUrl(this.infoSiteUrl, username).then(() => { return this.sitesProvider.updateSiteInfoByUrl(this.infoSiteUrl, username).then(() => {
// Reset fields so the data is not in the view anymore. // Reset fields so the data is not in the view anymore.

View File

@ -17,7 +17,7 @@
<div text-center padding> <div text-center padding>
<img src="assets/img/login_logo.png" class="avatar-full login-logo" role="presentation"> <img src="assets/img/login_logo.png" class="avatar-full login-logo" role="presentation">
</div> </div>
<form ion-list [formGroup]="siteForm" (ngSubmit)="connect($event, siteForm.value.siteUrl)" *ngIf="!fixedSites || fixedDisplay == 'select'"> <form ion-list [formGroup]="siteForm" (ngSubmit)="connect($event, siteForm.value.siteUrl)" *ngIf="!fixedSites || fixedDisplay == 'select'" #siteFormEl>
<!-- Form to input the site URL if there are no fixed sites. --> <!-- Form to input the site URL if there are no fixed sites. -->
<ng-container *ngIf="!fixedSites"> <ng-container *ngIf="!fixedSites">
<p padding>{{ 'core.login.newsitedescription' | translate }}</p> <p padding>{{ 'core.login.newsitedescription' | translate }}</p>

View File

@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component } from '@angular/core'; import { Component, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavController, ModalController, NavParams } from 'ionic-angular'; import { IonicPage, NavController, ModalController, NavParams } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider, CoreSiteCheckResponse } from '@providers/sites'; import { CoreSitesProvider, CoreSiteCheckResponse } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreConfigConstants } from '../../../../configconstants'; import { CoreConfigConstants } from '../../../../configconstants';
@ -31,6 +32,9 @@ import { CoreUrl } from '@classes/utils/url';
templateUrl: 'site.html', templateUrl: 'site.html',
}) })
export class CoreLoginSitePage { export class CoreLoginSitePage {
@ViewChild('siteFormEl') formElement: ElementRef;
siteForm: FormGroup; siteForm: FormGroup;
fixedSites: any[]; fixedSites: any[];
filteredSites: any[]; filteredSites: any[];
@ -38,9 +42,15 @@ export class CoreLoginSitePage {
showKeyboard = false; showKeyboard = false;
filter = ''; filter = '';
constructor(navParams: NavParams, private navCtrl: NavController, fb: FormBuilder, private appProvider: CoreAppProvider, constructor(navParams: NavParams,
private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider, protected navCtrl: NavController,
private modalCtrl: ModalController, private domUtils: CoreDomUtilsProvider) { fb: FormBuilder,
protected appProvider: CoreAppProvider,
protected sitesProvider: CoreSitesProvider,
protected loginHelper: CoreLoginHelperProvider,
protected modalCtrl: ModalController,
protected domUtils: CoreDomUtilsProvider,
protected eventsProvider: CoreEventsProvider) {
this.showKeyboard = !!navParams.get('showKeyboard'); this.showKeyboard = !!navParams.get('showKeyboard');
@ -96,6 +106,9 @@ export class CoreLoginSitePage {
// It's a demo site. // It's a demo site.
this.sitesProvider.getUserToken(siteData.url, siteData.username, siteData.password).then((data) => { this.sitesProvider.getUserToken(siteData.url, siteData.username, siteData.password).then((data) => {
return this.sitesProvider.newSite(data.siteUrl, data.token, data.privateToken).then(() => { return this.sitesProvider.newSite(data.siteUrl, data.token, data.privateToken).then(() => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, true);
return this.loginHelper.goToSiteInitialPage(); return this.loginHelper.goToSiteInitialPage();
}, (error) => { }, (error) => {
this.loginHelper.treatUserTokenError(siteData.url, error, siteData.username, siteData.password); this.loginHelper.treatUserTokenError(siteData.url, error, siteData.username, siteData.password);
@ -175,6 +188,9 @@ export class CoreLoginSitePage {
*/ */
protected async login(response: CoreSiteCheckResponse): Promise<void> { protected async login(response: CoreSiteCheckResponse): Promise<void> {
return this.sitesProvider.checkRequiredMinimumVersion(response.config).then(() => { return this.sitesProvider.checkRequiredMinimumVersion(response.config).then(() => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, true);
if (response.warning) { if (response.warning) {
this.domUtils.showErrorModal(response.warning, true, 4000); this.domUtils.showErrorModal(response.warning, true, 4000);
} }

View File

@ -1,5 +1,5 @@
<ion-card> <ion-card>
<form #f="ngForm" (ngSubmit)="submitForm($event)" role="search"> <form #f="ngForm" (ngSubmit)="submitForm($event)" role="search" #searchForm>
<ion-item> <ion-item>
<ion-input type="search" name="search" [(ngModel)]="searchText" [placeholder]="placeholder" [autocorrect]="autocorrect" [spellcheck]="spellcheck" [core-auto-focus]="autoFocus" [disabled]="disabled" role="searchbox" (focus)="showHistory()" (blur)="hideHistory()"></ion-input> <ion-input type="search" name="search" [(ngModel)]="searchText" [placeholder]="placeholder" [autocorrect]="autocorrect" [spellcheck]="spellcheck" [core-auto-focus]="autoFocus" [disabled]="disabled" role="searchbox" (focus)="showHistory()" (blur)="hideHistory()"></ion-input>
<button item-end ion-button clear icon-only type="submit" class="button-small" [attr.aria-label]="searchLabel" [disabled]="disabled || !searchText || (searchText.length < lengthCheck)"> <button item-end ion-button clear icon-only type="submit" class="button-small" [attr.aria-label]="searchLabel" [disabled]="disabled || !searchText || (searchText.length < lengthCheck)">

View File

@ -12,8 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core'; import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreSearchHistoryProvider, CoreSearchHistoryItem } from '../../providers/search-history'; import { CoreSearchHistoryProvider, CoreSearchHistoryItem } from '../../providers/search-history';
@ -47,9 +50,11 @@ export class CoreSearchBoxComponent implements OnInit, OnDestroy {
@Output() onSubmit: EventEmitter<string>; // Send data when submitting the search form. @Output() onSubmit: EventEmitter<string>; // Send data when submitting the search form.
@Output() onClear: EventEmitter<void>; // Send event when clearing the search form. @Output() onClear: EventEmitter<void>; // Send event when clearing the search form.
@ViewChild('searchForm') formElement: ElementRef;
searched = ''; // Last search emitted. searched = ''; // Last search emitted.
searchText = ''; searchText = '';
history: CoreSearchHistoryItem[]; history: CoreSearchHistoryItem[] = [];
historyShown = false; historyShown = false;
protected elementClicked = ''; protected elementClicked = '';
@ -58,6 +63,9 @@ export class CoreSearchBoxComponent implements OnInit, OnDestroy {
constructor(protected translate: TranslateService, constructor(protected translate: TranslateService,
protected utils: CoreUtilsProvider, protected utils: CoreUtilsProvider,
protected searchHistoryProvider: CoreSearchHistoryProvider, protected searchHistoryProvider: CoreSearchHistoryProvider,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider,
protected domUtils: CoreDomUtilsProvider,
) { ) {
this.onSubmit = new EventEmitter<string>(); this.onSubmit = new EventEmitter<string>();
this.onClear = new EventEmitter<void>(); this.onClear = new EventEmitter<void>();
@ -95,6 +103,8 @@ export class CoreSearchBoxComponent implements OnInit, OnDestroy {
this.saveSearchToHistory(this.searchText); this.saveSearchToHistory(this.searchText);
} }
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false, this.sitesProvider.getCurrentSiteId());
this.searched = this.searchText; this.searched = this.searchText;
this.onSubmit.emit(this.searchText); this.onSubmit.emit(this.searchText);
} }

View File

@ -64,6 +64,7 @@ export class CoreEventsProvider {
static SELECT_COURSE_TAB = 'select_course_tab'; static SELECT_COURSE_TAB = 'select_course_tab';
static WS_CACHE_INVALIDATED = 'ws_cache_invalidated'; static WS_CACHE_INVALIDATED = 'ws_cache_invalidated';
static SITE_STORAGE_DELETED = 'site_storage_deleted'; static SITE_STORAGE_DELETED = 'site_storage_deleted';
static FORM_ACTION = 'form_action';
protected logger; protected logger;
protected observables: { [s: string]: Subject<any> } = {}; protected observables: { [s: string]: Subject<any> } = {};

View File

@ -22,6 +22,7 @@ import { TranslateService } from '@ngx-translate/core';
import { CoreTextUtilsProvider } from './text'; import { CoreTextUtilsProvider } from './text';
import { CoreAppProvider } from '../app'; import { CoreAppProvider } from '../app';
import { CoreConfigProvider } from '../config'; import { CoreConfigProvider } from '../config';
import { CoreEventsProvider } from '../events';
import { CoreLoggerProvider } from '../logger'; import { CoreLoggerProvider } from '../logger';
import { CoreUrlUtilsProvider } from './url'; import { CoreUrlUtilsProvider } from './url';
import { CoreFileProvider } from '@providers/file'; import { CoreFileProvider } from '@providers/file';
@ -64,20 +65,21 @@ export class CoreDomUtilsProvider {
protected displayedAlerts = {}; // To prevent duplicated alerts. protected displayedAlerts = {}; // To prevent duplicated alerts.
protected logger; protected logger;
constructor(private translate: TranslateService, constructor(protected translate: TranslateService,
private loadingCtrl: LoadingController, protected loadingCtrl: LoadingController,
private toastCtrl: ToastController, protected toastCtrl: ToastController,
private alertCtrl: AlertController, protected alertCtrl: AlertController,
private textUtils: CoreTextUtilsProvider, protected textUtils: CoreTextUtilsProvider,
private appProvider: CoreAppProvider, protected appProvider: CoreAppProvider,
private platform: Platform, protected platform: Platform,
private configProvider: CoreConfigProvider, protected configProvider: CoreConfigProvider,
private urlUtils: CoreUrlUtilsProvider, protected urlUtils: CoreUrlUtilsProvider,
private modalCtrl: ModalController, protected modalCtrl: ModalController,
private sanitizer: DomSanitizer, protected sanitizer: DomSanitizer,
private popoverCtrl: PopoverController, protected popoverCtrl: PopoverController,
private fileProvider: CoreFileProvider, protected fileProvider: CoreFileProvider,
loggerProvider: CoreLoggerProvider) { loggerProvider: CoreLoggerProvider,
protected eventsProvider: CoreEventsProvider) {
this.logger = loggerProvider.getInstance('CoreDomUtilsProvider'); this.logger = loggerProvider.getInstance('CoreDomUtilsProvider');
@ -1625,4 +1627,32 @@ export class CoreDomUtilsProvider {
// Now move the element into the wrapper. // Now move the element into the wrapper.
wrapper.appendChild(el); wrapper.appendChild(el);
} }
/**
* Trigger form cancelled event.
*
* @param form Form element.
* @param siteId The site affected. If not provided, no site affected.
*/
triggerFormCancelledEvent(form: HTMLElement, siteId?: string): void {
this.eventsProvider.trigger(CoreEventsProvider.FORM_ACTION, {
action: 'cancel',
form: form,
}, siteId);
}
/**
* Trigger form submitted event.
*
* @param form Form element.
* @param online Whether the action was done in offline or not.
* @param siteId The site affected. If not provided, no site affected.
*/
triggerFormSubmittedEvent(form: HTMLElement, online?: boolean, siteId?: string): void {
this.eventsProvider.trigger(CoreEventsProvider.FORM_ACTION, {
action: 'submit',
form: form,
online: !!online,
}, siteId);
}
} }

View File

@ -134,11 +134,6 @@ $link-color: $blue !default;
$background-color: $gray-light !default; $background-color: $gray-light !default;
$subdued-text-color: $gray-darker !default; $subdued-text-color: $gray-darker !default;
$core-warning-color: colors($colors, warning) !default; // yellow.
$core-success-color: colors($colors, success) !default; // green.
$core-info-color: colors($colors, info) !default; // / blue.
$core-error-color: colors($colors, alert) !default; // Red.
$list-background-color: $white !default; $list-background-color: $white !default;
$tabs-background: $gray-darker !default; $tabs-background: $gray-darker !default;