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.downloading": "local_moodlemobileapp",
"core.edit": "moodle",
"core.editor.autosavesucceeded": "editor_atto",
"core.editor.textrecovered": "editor_atto",
"core.emptysplit": "local_moodlemobileapp",
"core.error": "moodle",
"core.errorchangecompletion": "local_moodlemobileapp",

View File

@ -9,7 +9,7 @@
</ion-refresher>
<core-loading [hideUntil]="loaded">
<form ion-list [formGroup]="eventForm" *ngIf="!error">
<form ion-list [formGroup]="eventForm" *ngIf="!error" #editEventForm>
<!-- Event name. -->
<ion-item text-wrap>
<ion-label stacked><h2 [core-mark-required]="true">{{ 'addon.calendar.eventname' | translate }}</h2></ion-label>
@ -86,7 +86,7 @@
<!-- Description. -->
<ion-item text-wrap>
<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>
<!-- Location. -->

View File

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

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// 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 { IonicPage, NavController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
@ -25,7 +25,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
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 { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
import { AddonCalendarHelperProvider } from '../../providers/helper';
@ -43,7 +43,8 @@ import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
})
export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
@ViewChild(CoreRichTextEditorComponent) descriptionEditor: CoreRichTextEditorComponent;
@ViewChild(CoreEditorRichTextEditorComponent) descriptionEditor: CoreEditorRichTextEditorComponent;
@ViewChild('editEventForm') formElement: ElementRef;
title: string;
dateFormat: string;
@ -496,6 +497,8 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
this.calendarProvider.submitEvent(this.eventId, data).then((result) => {
event = result.event;
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, result.sent, this.currentSite.getId());
if (result.sent) {
// Event created or edited, invalidate right days & months.
const numberOfRepetitions = formData.repeat ? formData.repeats :
@ -557,6 +560,9 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
discard(): void {
this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => {
this.calendarOffline.deleteEvent(this.eventId).then(() => {
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.currentSite.getId());
this.returnToList();
}).catch(() => {
// Shouldn't happen.
@ -572,16 +578,18 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
*
* @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)) {
// Show confirmation if some data has been modified.
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
} else {
return Promise.resolve();
await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.currentSite.getId());
}
/**
* Unblock sync.
*/
protected unblockSync(): void {
if (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 { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -31,7 +32,8 @@ import { CoreDirectivesModule } from '@directives/directives.module';
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
CoreDirectivesModule,
CoreEditorComponentsModule,
],
providers: [
AddonModAssignFeedbackCommentsHandler

View File

@ -19,5 +19,6 @@
<!-- Edit -->
<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>

View File

@ -9,7 +9,7 @@
</ion-navbar>
</ion-header>
<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>
<button ion-button block (click)="done($event)">{{ 'core.done' | translate }}</button>
</form>

View File

@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and
// 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 { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegate';
import {
@ -36,10 +38,17 @@ export class AddonModAssignEditFeedbackModalPage {
@Input() plugin: AddonModAssignPlugin; // The plugin object.
@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.
constructor(params: NavParams, protected viewCtrl: ViewController, protected domUtils: CoreDomUtilsProvider,
protected translate: TranslateService, protected feedbackDelegate: AddonModAssignFeedbackDelegate) {
constructor(params: NavParams,
protected viewCtrl: ViewController,
protected domUtils: CoreDomUtilsProvider,
protected translate: TranslateService,
protected feedbackDelegate: AddonModAssignFeedbackDelegate,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider) {
this.assign = params.get('assign');
this.submission = params.get('submission');
@ -52,16 +61,17 @@ export class AddonModAssignEditFeedbackModalPage {
*
* @return Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) {
return true;
return;
}
return this.hasDataChanged().then((changed) => {
if (changed) {
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}
});
const changed = await this.hasDataChanged();
if (changed) {
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.stopPropagation();
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false, this.sitesProvider.getCurrentSiteId());
// Close the modal, sending the input data.
this.forceLeave = true;
this.closeModal(this.getInputData());

View File

@ -13,7 +13,7 @@
<core-loading [hideUntil]="loaded">
<ion-list>
<!-- @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. -->
<ion-item text-wrap *ngIf="submissionStatement">
<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
// 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 { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
@ -34,6 +34,9 @@ import { AddonModAssignHelperProvider } from '../../providers/helper';
templateUrl: 'edit.html',
})
export class AddonModAssignEditPage implements OnInit, OnDestroy {
@ViewChild('editSubmissionForm') formElement: ElementRef;
title: string; // Title to display.
assign: AddonModAssignAssign; // Assignment.
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.
*/
ionViewCanLeave(): boolean | Promise<void> {
async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) {
return true;
return;
}
// Check if data has changed.
return this.hasDataChanged().then((changed) => {
if (changed) {
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}
}).then(() => {
// Nothing has changed or user confirmed to leave. Clear temporary data from plugins.
this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, this.getInputData());
});
const changed = await this.hasDataChanged();
if (changed) {
await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}
// Nothing has changed or user confirmed to leave. Clear temporary data from plugins.
this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, this.getInputData());
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
}
/**
@ -265,69 +269,74 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy {
*
* @return Promise resolved when done.
*/
protected saveSubmission(): Promise<any> {
protected async saveSubmission(): Promise<void> {
const inputData = this.getInputData();
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 size;
// 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.
return -1;
}).then((size) => {
modal.dismiss();
size = -1;
}
modal.dismiss();
try {
// Confirm action.
return this.fileUploaderHelper.confirmUploadFile(size, true, this.allowOffline);
}).then(() => {
await this.fileUploaderHelper.confirmUploadFile(size, true, this.allowOffline);
modal = this.domUtils.showModalLoading('core.sending', true);
return this.prepareSubmissionData(inputData).then((pluginData) => {
if (!Object.keys(pluginData).length) {
// Nothing to save.
return;
}
const pluginData = await this.prepareSubmissionData(inputData);
if (!Object.keys(pluginData).length) {
// Nothing to save.
return;
}
let promise;
let sent: boolean;
if (this.saveOffline) {
// Save submission in offline.
promise = this.assignOfflineProvider.saveSubmission(this.assign.id, this.courseId, pluginData,
this.userSubmission.timemodified, !this.assign.submissiondrafts, this.userId);
} else {
// Try to send it to server.
promise = this.assignProvider.saveSubmission(this.assign.id, this.courseId, pluginData, this.allowOffline,
this.userSubmission.timemodified, !!this.assign.submissiondrafts, this.userId);
}
if (this.saveOffline) {
// Save submission in offline.
sent = false;
await this.assignOfflineProvider.saveSubmission(this.assign.id, this.courseId, pluginData,
this.userSubmission.timemodified, !this.assign.submissiondrafts, this.userId);
} else {
// Try to send it to server.
sent = await this.assignProvider.saveSubmission(this.assign.id, this.courseId, pluginData, this.allowOffline,
this.userSubmission.timemodified, !!this.assign.submissiondrafts, this.userId);
}
return promise.then(() => {
// Clear temporary data from plugins.
return this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, inputData);
}).then(() => {
// Submission saved, trigger event.
const params = {
assignmentId: this.assign.id,
submissionId: this.userSubmission.id,
userId: this.userId,
};
// Clear temporary data from plugins.
await this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, inputData);
this.eventsProvider.trigger(AddonModAssignProvider.SUBMISSION_SAVED_EVENT, params,
this.sitesProvider.getCurrentSiteId());
// Submission saved, trigger events.
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, sent, this.sitesProvider.getCurrentSiteId());
if (!this.assign.submissiondrafts) {
// No drafts allowed, so it was submitted. Trigger event.
this.eventsProvider.trigger(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, params,
this.sitesProvider.getCurrentSiteId());
}
});
});
}).finally(() => {
const params = {
assignmentId: this.assign.id,
submissionId: this.userSubmission.id,
userId: this.userId,
};
this.eventsProvider.trigger(AddonModAssignProvider.SUBMISSION_SAVED_EVENT, params,
this.sitesProvider.getCurrentSiteId());
if (!this.assign.submissiondrafts) {
// No drafts allowed, so it was submitted. Trigger event.
this.eventsProvider.trigger(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, params,
this.sitesProvider.getCurrentSiteId());
}
} finally {
modal.dismiss();
});
}
}
/**

View File

@ -15,6 +15,6 @@
<p>{{ 'core.numwords' | translate: {'$a': words + ' / ' + configs.wordlimit} }}</p>
</ion-item>
<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>
</div>

View File

@ -14,6 +14,7 @@
import { Component, OnInit, ElementRef } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { AddonModAssignProvider } from '../../../providers/assign';
@ -35,16 +36,23 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS
text: string;
loaded: boolean;
wordLimitEnabled: boolean;
currentUserId: number;
protected wordCountTimeout: any;
protected element: HTMLElement;
constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider,
protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider,
element: ElementRef) {
constructor(
protected fb: FormBuilder,
protected domUtils: CoreDomUtilsProvider,
protected textUtils: CoreTextUtilsProvider,
protected assignProvider: AddonModAssignProvider,
protected assignOfflineProvider: AddonModAssignOfflineProvider,
element: ElementRef,
sitesProvider: CoreSitesProvider) {
super();
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 { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -31,7 +32,8 @@ import { CoreDirectivesModule } from '@directives/directives.module';
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
CoreDirectivesModule,
CoreEditorComponentsModule,
],
providers: [
AddonModAssignSubmissionOnlineTextHandler

View File

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

View File

@ -2,7 +2,7 @@
<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>
<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>
</span>

View File

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

View File

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

View File

@ -21,7 +21,7 @@
<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>
<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>
</form>
</div>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// 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 { TranslateService } from '@ngx-translate/core';
import { FormGroup } from '@angular/forms';
@ -40,6 +40,7 @@ import { CoreTagProvider } from '@core/tag/providers/tag';
})
export class AddonModDataEditPage {
@ViewChild(Content) content: Content;
@ViewChild('editFormEl') formElement: ElementRef;
protected module: any;
protected courseId: number;
@ -95,28 +96,25 @@ export class AddonModDataEditPage {
*
* @return Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
async ionViewCanLeave(): Promise<void> {
if (this.forceLeave || !this.entry) {
return true;
return;
}
const inputData = this.editForm.value;
return this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id,
this.entry.contents).then((changed) => {
if (!changed) {
return Promise.resolve();
}
const changed = await this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, this.entry.contents);
if (changed) {
// 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.
return this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id,
this.entry.contents).then((files) => {
this.fileUploaderProvider.clearTmpFiles(files);
});
});
await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}
// Delete the local files from the tmp folder.
const files = await this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id, this.entry.contents);
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.
if ((this.entryId && result.updated) || (!this.entryId && result.newentryid)) {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, result.sent, this.siteId);
const promises = [];
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(true)">{{ 'addon.mod_data.advancedsearch' | translate }}</a>
</div>
<form (ngSubmit)="searchEntries($event)" [formGroup]="searchForm">
<form (ngSubmit)="searchEntries($event)" [formGroup]="searchForm" #searchFormEl>
<ion-list no-margin>
<ion-item [hidden]="search.searchingAdvanced">
<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
// limitations under the License.
import { Component } from '@angular/core';
import { Component, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavParams, ViewController } from 'ionic-angular';
import { FormBuilder, FormGroup } from '@angular/forms';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
@ -32,6 +34,8 @@ import { CoreTagProvider } from '@core/tag/providers/tag';
templateUrl: 'search.html',
})
export class AddonModDataSearchPage {
@ViewChild('searchFormEl') formElement: ElementRef;
search: any;
fields: any;
data: any;
@ -41,10 +45,17 @@ export class AddonModDataSearchPage {
jsData: any;
fieldsArray: any;
constructor(params: NavParams, private viewCtrl: ViewController, fb: FormBuilder, protected utils: CoreUtilsProvider,
protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate,
protected textUtils: CoreTextUtilsProvider, protected dataHelper: AddonModDataHelperProvider,
private tagProvider: CoreTagProvider) {
constructor(params: NavParams,
protected viewCtrl: ViewController,
fb: FormBuilder,
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.fields = params.get('fields');
this.data = params.get('data');
@ -175,6 +186,12 @@ export class AddonModDataSearchPage {
* @param data Data to return to the page.
*/
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);
}

View File

@ -126,7 +126,8 @@ export class AddonModDataProvider {
.then((entry) => {
return {
// Return provissional entry Id.
newentryid: entry
newentryid: entry,
sent: false,
};
});
};
@ -142,7 +143,11 @@ export class AddonModDataProvider {
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)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error);
@ -194,7 +199,12 @@ export class AddonModDataProvider {
const storeOffline = (): Promise<any> => {
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.
@ -210,7 +220,11 @@ export class AddonModDataProvider {
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)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error);
@ -288,7 +302,12 @@ export class AddonModDataProvider {
// Convenience function to store a data to be synchronized later.
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;
@ -318,7 +337,11 @@ export class AddonModDataProvider {
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)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error);
@ -368,7 +391,8 @@ export class AddonModDataProvider {
return this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId)
.then(() => {
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)
.then((result) => {
result.updated = true;
result.sent = true;
return result;
});
@ -418,7 +443,11 @@ export class AddonModDataProvider {
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)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error);

View File

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

View File

@ -64,7 +64,7 @@
</ion-item>
<ion-item>
<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 text-wrap *ngIf="accessInfo.canpostprivatereply">
<ion-label>{{ 'addon.mod_forum.privatereply' | translate }}</ion-label>

View File

@ -16,7 +16,7 @@
</ion-item>
<ion-item>
<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-divider text-wrap (click)="toggleAdvanced()" class="core-expandable">
<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 { AddonModForumComponentsModule } from '../../components/components.module';
import { AddonModForumEditPostPage } from './edit-post';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -28,6 +29,7 @@ import { AddonModForumEditPostPage } from './edit-post';
CoreComponentsModule,
CoreDirectivesModule,
AddonModForumComponentsModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModForumEditPostPage),
TranslateModule.forChild()
],

View File

@ -19,7 +19,7 @@
</ion-item>
<ion-item>
<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-divider text-wrap (click)="toggleAdvanced()" class="core-expandable">
<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 { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModForumNewDiscussionPage } from './new-discussion';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -26,6 +27,7 @@ import { AddonModForumNewDiscussionPage } from './new-discussion';
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModForumNewDiscussionPage),
TranslateModule.forChild()
],

View File

@ -25,7 +25,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
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 { AddonModForumOfflineProvider } from '../../providers/offline';
import { AddonModForumHelperProvider } from '../../providers/helper';
@ -41,7 +41,7 @@ import { AddonModForumSyncProvider } from '../../providers/sync';
})
export class AddonModForumNewDiscussionPage implements OnDestroy {
@ViewChild(CoreRichTextEditorComponent) messageEditor: CoreRichTextEditorComponent;
@ViewChild(CoreEditorRichTextEditorComponent) messageEditor: CoreEditorRichTextEditorComponent;
component = AddonModForumProvider.COMPONENT;
messageControl = new FormControl();

View File

@ -15,7 +15,7 @@
</ion-item>
<ion-item>
<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 *ngIf="categories.length > 0">
<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 { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModGlossaryEditPage } from './edit';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -26,6 +27,7 @@ import { AddonModGlossaryEditPage } from './edit';
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModGlossaryEditPage),
TranslateModule.forChild()
],

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@
</ion-navbar>
</ion-header>
<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>
<core-show-password item-content [name]="'password'">
<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
// limitations under the License.
import { Component } from '@angular/core';
import { Component, ViewChild, ElementRef } from '@angular/core';
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.
@ -24,8 +27,12 @@ import { IonicPage, ViewController } from 'ionic-angular';
templateUrl: 'password-modal.html',
})
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.
@ -37,6 +44,8 @@ export class AddonModLessonPasswordModalPage {
e.preventDefault();
e.stopPropagation();
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false, this.sitesProvider.getCurrentSiteId());
this.viewCtrl.dismiss(password.value);
}
@ -44,6 +53,8 @@ export class AddonModLessonPasswordModalPage {
* Close modal.
*/
closeModal(): void {
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
this.viewCtrl.dismiss();
}
}

View File

@ -38,7 +38,7 @@
<!-- Question page. -->
<!-- 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">
<core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="pageContent" contextLevel="module" [contextInstanceId]="lesson.coursemodule" [courseId]="courseId"></core-format-text>
</ion-item-divider>
@ -57,7 +57,7 @@
<!-- Essay. -->
<ng-container *ngSwitchCase="'essay'">
<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 text-wrap *ngIf="!question.textarea && question.useranswer">
<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 { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModLessonPlayerPage } from './player';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -26,6 +27,7 @@ import { AddonModLessonPlayerPage } from './player';
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModLessonPlayerPage),
TranslateModule.forChild()
],

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// 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 { IonicPage, NavParams, Content, PopoverController, ModalController, Modal, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
@ -41,6 +41,7 @@ import { AddonModLessonHelperProvider } from '../../providers/helper';
})
export class AddonModLessonPlayerPage implements OnInit, OnDestroy {
@ViewChild(Content) content: Content;
@ViewChild('questionFormEl') formElement: ElementRef;
component = AddonModLessonProvider.COMPONENT;
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.
*/
ionViewCanLeave(): boolean | Promise<void> {
async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) {
return true;
return;
}
if (this.question && !this.eolData && !this.processData && this.originalData) {
// Question shown. Check if there is any change.
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.
*
* @param data The data to send.
* @param formSubmitted Whether a form was submitted.
* @return Promise resolved when done.
*/
protected processPage(data: any): Promise<any> {
protected processPage(data: any, formSubmitted?: boolean): Promise<any> {
this.loaded = false;
const args = [this.lesson, this.courseId, this.pageData, data, this.password, this.review, this.offline, this.accessInfo,
this.jumps];
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)) {
// Lesson allows offline and the user changed some data in server. Update cached data.
const retake = this.accessInfo.attemptscount;
@ -637,7 +644,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy {
// Use getRawValue to include disabled values.
const data = this.lessonHelper.prepareQuestionData(this.question, this.questionForm.getRawValue());
this.processPage(data).finally(() => {
this.processPage(data, true).finally(() => {
this.loaded = true;
});
}

View File

@ -3089,6 +3089,7 @@ export class AddonModLessonProvider {
result.warnings = [];
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.sent = false;
Object.assign(result, calculatedData);
return result;
@ -3104,6 +3105,8 @@ export class AddonModLessonProvider {
review: review
}, this.sitesProvider.getCurrentSiteId());
response.sent = true;
return response;
});
}

View File

@ -45,7 +45,7 @@
</div>
<!-- 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">
<ion-card id="addon-mod_quiz-question-{{question.slot}}">
<!-- "Header" of the question. -->

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// 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 { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
@ -41,6 +41,7 @@ import { Subscription } from 'rxjs';
export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
@ViewChild(Content) content: Content;
@ViewChildren(CoreQuestionComponent) questionComponents: QueryList<CoreQuestionComponent>;
@ViewChild('quizForm') formElement: ElementRef;
quiz: any; // The quiz the attempt belongs to.
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.
*/
ionViewCanLeave(): boolean | Promise<void> {
async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) {
return true;
return;
}
if (this.questions && this.questions.length && !this.showSummary) {
// Save answers.
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.
modal.dismiss();
return this.domUtils.showConfirm(this.translate.instant('addon.mod_quiz.confirmleavequizonerror'));
}).finally(() => {
modal.dismiss();
});
}
await this.domUtils.showConfirm(this.translate.instant('addon.mod_quiz.confirmleavequizonerror'));
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.
this.autoSave.cancelAutoSave();
this.autoSave.hideAutoSaveError();
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, !this.offline,
this.sitesProvider.getCurrentSiteId());
});
}

View File

@ -10,7 +10,7 @@
</ion-header>
<ion-content padding class="addon-mod_quiz-preflight-modal">
<core-loading [hideUntil]="loaded">
<form ion-list [formGroup]="preflightForm" (ngSubmit)="sendData($event)">
<form ion-list [formGroup]="preflightForm" (ngSubmit)="sendData($event)" #preflightFormEl>
<!-- Access rules. -->
<ng-container *ngFor="let data of accessRulesData; let last = last">
<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
// 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 { TranslateService } from '@ngx-translate/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate';
@ -31,6 +32,7 @@ import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-del
export class AddonModQuizPreflightModalPage implements OnInit {
@ViewChild(Content) content: Content;
@ViewChild('preflightFormEl') formElement: ElementRef;
preflightForm: FormGroup;
title: string;
@ -43,9 +45,15 @@ export class AddonModQuizPreflightModalPage implements OnInit {
protected siteId: string;
protected rules: string[];
constructor(params: NavParams, fb: FormBuilder, translate: TranslateService, sitesProvider: CoreSitesProvider,
protected viewCtrl: ViewController, protected accessRuleDelegate: AddonModQuizAccessRuleDelegate,
protected injector: Injector, protected domUtils: CoreDomUtilsProvider) {
constructor(params: NavParams,
fb: FormBuilder,
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.quiz = params.get('quiz');
@ -112,6 +120,8 @@ export class AddonModQuizPreflightModalPage implements OnInit {
this.domUtils.showErrorModal('core.errorinvalidform', true);
}
} else {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false, this.siteId);
this.viewCtrl.dismiss(this.preflightForm.value);
}
}
@ -120,6 +130,8 @@ export class AddonModQuizPreflightModalPage implements OnInit {
* Close modal.
*/
closeModal(): void {
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.siteId);
this.viewCtrl.dismiss();
}
}

View File

@ -11,13 +11,13 @@
</ion-header>
<ion-content>
<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-input name="title" type="text" [placeholder]="'addon.mod_wiki.newpagetitle' | translate" [formControlName]="'title'"></ion-input>
</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 *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 { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModWikiEditPage } from './edit';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -26,6 +27,7 @@ import { AddonModWikiEditPage } from './edit';
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModWikiEditPage),
TranslateModule.forChild()
],

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// 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 { FormControl, FormGroup, FormBuilder } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
@ -37,6 +37,8 @@ import { AddonModWikiSyncProvider, AddonModWikiSyncSubwikiResult } from '../../p
})
export class AddonModWikiEditPage implements OnInit, OnDestroy {
@ViewChild('editPageForm') formElement: ElementRef;
title: string; // Title to display.
pageForm: FormGroup; // The form group.
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.
componentId: number; // Component ID to link the files to.
wrongVersionLock: boolean; // Whether the page lock doesn't match the initial one.
editorExtraParams: {[name: string]: any} = {};
protected module: any; // Wiki module instance.
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.
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.
*/
ionViewCanLeave(): boolean | Promise<void> {
async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) {
return true;
return;
}
// Check if data has changed.
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) {
// Edit existing page.
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.
return this.wikiProvider.invalidatePage(this.pageId).then(() => {
return this.gotoPage(title);
@ -441,6 +462,10 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy {
let wikiId = this.wikiId || (this.module && this.module.instance);
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) {
// Page was created, get its data and go to the page.
this.pageId = id;

View File

@ -1,6 +1,6 @@
<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">
<ng-container *ngIf="componentClass && assessmentStrategyLoaded">
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
@ -16,7 +16,7 @@
</ion-item>
<ion-item stacked *ngIf="edit">
<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>
</ion-item>
<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
// 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 { TranslateService } from '@ngx-translate/core';
import { CoreSyncProvider } from '@providers/sync';
@ -44,6 +44,8 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit {
@Input() strategy: string;
@Input() edit?: boolean;
@ViewChild('assessmentForm') formElement: ElementRef;
componentClass: any;
data = {
workshopId: 0,
@ -292,7 +294,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit {
// Save assessment in offline.
return this.workshopOffline.saveAssessment(this.workshop.id, this.assessmentId, this.workshop.course,
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,
assessmentData, false, allowOffline);
}).then((grade) => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, !!grade, this.sitesProvider.getCurrentSiteId());
const promises = [];
// 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 { AddonModWorkshopAssessmentComponent } from './assessment/assessment';
import { AddonModWorkshopAssessmentStrategyComponent } from './assessment-strategy/assessment-strategy';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -39,7 +40,8 @@ import { AddonModWorkshopAssessmentStrategyComponent } from './assessment-strate
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
CoreCourseComponentsModule
CoreCourseComponentsModule,
CoreEditorComponentsModule,
],
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>
<form ion-list [formGroup]="evaluateForm" *ngIf="evaluating">
<form ion-list [formGroup]="evaluateForm" *ngIf="evaluating" #evaluateFormEl>
<ion-item text-wrap>
<h2>{{ 'addon.mod_workshop.assessmentsettings' | translate }}</h2>
</ion-item>
@ -60,7 +60,7 @@
</ion-item>
<ion-item *ngIf="access.canoverridegrades">
<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>
</form>
<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 { AddonModWorkshopComponentsModule } from '../../components/components.module';
import { AddonModWorkshopAssessmentPage } from './assessment';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -28,6 +29,7 @@ import { AddonModWorkshopAssessmentPage } from './assessment';
CoreDirectivesModule,
CoreComponentsModule,
AddonModWorkshopComponentsModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModWorkshopAssessmentPage),
TranslateModule.forChild()
],

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// 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 { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
@ -39,6 +39,8 @@ import { AddonModWorkshopSyncProvider } from '../../providers/sync';
})
export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy {
@ViewChild('evaluateFormEl') formElement: ElementRef;
assessment: any;
submission: any;
profile: any;
@ -118,17 +120,19 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy {
*
* @return Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
async ionViewCanLeave(): Promise<void> {
if (this.forceLeave || !this.evaluating) {
return true;
return;
}
if (!this.hasEvaluationChanged()) {
return Promise.resolve();
return;
}
// 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.
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 = {
workshopId: this.workshopId,
assessmentId: this.assessmentId,

View File

@ -10,7 +10,7 @@
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<form ion-list [formGroup]="editForm" *ngIf="workshop">
<form ion-list [formGroup]="editForm" *ngIf="workshop" #editFormEl>
<ion-item text-wrap>
<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>
@ -18,7 +18,7 @@
<ion-item *ngIf="textAvailable">
<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>
<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 { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModWorkshopEditSubmissionPage } from './edit-submission';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -26,6 +27,7 @@ import { AddonModWorkshopEditSubmissionPage } from './edit-submission';
imports: [
CoreDirectivesModule,
CoreComponentsModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModWorkshopEditSubmissionPage),
TranslateModule.forChild()
],

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// 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 { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
@ -37,6 +37,8 @@ import { AddonModWorkshopOfflineProvider } from '../../providers/offline';
})
export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
@ViewChild('editFormEl') formElement: ElementRef;
module: any;
courseId: number;
access: any;
@ -51,6 +53,7 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
component = AddonModWorkshopProvider.COMPONENT;
componentId: number;
editForm: FormGroup; // The form group.
editorExtraParams: {[name: string]: any} = {};
protected workshopId: number;
protected submissionId: number;
@ -86,6 +89,10 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
this.editForm = new FormGroup({});
this.editForm.addControl('title', this.fb.control('', Validators.required));
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.
*/
ionViewCanLeave(): boolean | Promise<void> {
async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) {
return true;
return;
}
let promise;
// Check if data has changed.
if (!this.hasDataChanged()) {
promise = Promise.resolve();
} else {
if (this.hasDataChanged()) {
// 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) {
// Delete the local files from the tmp folder.
this.fileUploaderProvider.clearTmpFiles(this.submission.attachmentfiles);
}
});
if (this.submission.attachmentfiles) {
// Delete the local files from the tmp folder.
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.
return this.workshopOffline.saveSubmission(this.workshopId, this.courseId, inputData.title,
inputData.content, attachmentsId, submissionId, 'update').then(() => {
// Don't return anything.
return false;
});
}
@ -360,8 +363,8 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
if (saveOffline) {
// Save submission in offline.
return this.workshopOffline.saveSubmission(this.workshopId, this.courseId, inputData.title, inputData.content,
attachmentsId, submissionId, 'add').then(() => {
// Don't return anything.
attachmentsId, submissionId, 'add').then(() => {
return false;
});
}
@ -370,6 +373,9 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
return this.workshopProvider.addSubmission(this.workshopId, this.courseId, inputData.title, inputData.content,
attachmentsId, undefined, submissionId, allowOffline);
}).then((newSubmissionId) => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, !!newSubmissionId, this.siteId);
const data = {
workshopId: this.workshopId,
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>
</ion-list>
<form ion-list [formGroup]="feedbackForm" *ngIf="canAddFeedback">
<form ion-list [formGroup]="feedbackForm" *ngIf="canAddFeedback" #feedbackFormEl>
<ion-item text-wrap>
<h2>{{ 'addon.mod_workshop.feedbackauthor' | translate }}</h2>
</ion-item>
@ -87,7 +87,7 @@
</ion-item>
<ion-item>
<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>
</form>

View File

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

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// 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 { FormGroup, FormBuilder } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
@ -41,6 +41,7 @@ import { AddonModWorkshopSyncProvider } from '../../providers/sync';
export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy {
@ViewChild(AddonModWorkshopAssessmentStrategyComponent) assessmentStrategy: AddonModWorkshopAssessmentStrategyComponent;
@ViewChild('feedbackFormEl') formElement: ElementRef;
module: any;
workshop: any;
@ -143,14 +144,16 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy {
*
* @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();
if (this.forceLeave || (!this.hasEvaluationChanged() && !assessmentHasChanged)) {
return true;
return;
}
// 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.
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 = {
workshopId: this.workshopId,
cmId: this.module.cmid,

View File

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

View File

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

View File

@ -11,7 +11,7 @@
<!-- 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>
<!-- 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>
<!-- Draft files not supported. -->

View File

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

View File

@ -9,5 +9,5 @@
<span [core-mark-required]="field.required">{{ field.name }}</span>
<core-input-errors [control]="control"></core-input-errors>
</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>

View File

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

View File

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

View File

@ -1482,6 +1482,8 @@
"core.downloaded": "Downloaded",
"core.downloading": "Downloading",
"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.error": "Error",
"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 { CoreTabsComponent } from './tabs/tabs';
import { CoreTabComponent } from './tabs/tab';
import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor';
import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
import { CoreSendMessageFormComponent } from './send-message-form/send-message-form';
@ -79,7 +78,6 @@ import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
CoreSitePickerComponent,
CoreTabsComponent,
CoreTabComponent,
CoreRichTextEditorComponent,
CoreNavBarButtonsComponent,
CoreDynamicComponent,
CoreSendMessageFormComponent,
@ -128,7 +126,6 @@ import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
CoreSitePickerComponent,
CoreTabsComponent,
CoreTabComponent,
CoreRichTextEditorComponent,
CoreNavBarButtonsComponent,
CoreDynamicComponent,
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>
<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
// 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 { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
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() onClick?: EventEmitter<void>; // Will notify when the file is clicked. Only if overrideClick is true.
@ViewChild('nameForm') formElement: ElementRef;
fileName: string;
fileIcon: string;
fileExtension: string;
@ -47,12 +51,14 @@ export class CoreLocalFileComponent implements OnInit {
editMode: boolean;
relativePath: string;
constructor(private mimeUtils: CoreMimetypeUtilsProvider,
private utils: CoreUtilsProvider,
private textUtils: CoreTextUtilsProvider,
private fileProvider: CoreFileProvider,
private domUtils: CoreDomUtilsProvider,
private timeUtils: CoreTimeUtilsProvider) {
constructor(protected mimeUtils: CoreMimetypeUtilsProvider,
protected utils: CoreUtilsProvider,
protected textUtils: CoreTextUtilsProvider,
protected fileProvider: CoreFileProvider,
protected domUtils: CoreDomUtilsProvider,
protected timeUtils: CoreTimeUtilsProvider,
protected sitesProvider: CoreSitesProvider,
protected eventsProvider: CoreEventsProvider) {
this.onDelete = new EventEmitter();
this.onRename = new EventEmitter();
this.onClick = new EventEmitter();
@ -137,6 +143,7 @@ export class CoreLocalFileComponent implements OnInit {
if (newName == this.file.name) {
// Name hasn't changed, stop.
this.editMode = false;
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
return;
}
@ -152,6 +159,10 @@ export class CoreLocalFileComponent implements OnInit {
}).catch(() => {
// File doesn't exist, move it.
return this.fileProvider.moveFile(this.relativePath, newPath).then((fileEntry) => {
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false,
this.sitesProvider.getCurrentSiteId());
this.editMode = false;
this.file = fileEntry;
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>
<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)">

View File

@ -12,13 +12,14 @@
// See the License for the specific language governing permissions and
// 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 { CoreConfigProvider } from '@providers/config';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
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() onResize: EventEmitter<void>; // Emit when resizing the textarea.
@ViewChild('messageForm') formElement: ElementRef;
protected sendOnEnter: boolean;
constructor(private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider, configProvider: CoreConfigProvider,
eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider) {
constructor(protected utils: CoreUtilsProvider,
protected textUtils: CoreTextUtilsProvider,
configProvider: CoreConfigProvider,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider,
protected appProvider: CoreAppProvider,
protected domUtils: CoreDomUtilsProvider) {
this.onSubmit = new EventEmitter();
this.onResize = new EventEmitter();
@ -82,6 +90,8 @@ export class CoreSendMessageFormComponent implements OnInit {
this.message = ''; // Reset the form.
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false, this.sitesProvider.getCurrentSiteId());
value = this.textUtils.replaceNewLines(value, '<br>');
this.onSubmit.emit(value);
}

View File

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

View File

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

View File

@ -54,7 +54,7 @@ export class CoreCommentsProvider {
// Convenience function to store a comment to be synchronized later.
const storeOffline = (): Promise<any> => {
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-header>
<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>
<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>

View File

@ -12,8 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { Component, ViewChild, ElementRef } from '@angular/core';
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.
@ -24,12 +27,19 @@ import { IonicPage, ViewController } from 'ionic-angular';
templateUrl: 'self-enrol-password.html',
})
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(): void {
this.domUtils.triggerFormCancelledEvent(this.formElement.nativeElement, this.sitesProvider.getCurrentSiteId());
this.viewCtrl.dismiss();
}
@ -43,6 +53,8 @@ export class CoreCoursesSelfEnrolPasswordPage {
e.preventDefault();
e.stopPropagation();
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false, this.sitesProvider.getCurrentSiteId());
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,7 +1,14 @@
<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 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>
<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">
<button *ngIf="toolbarArrows" class="toolbar-arrow" [class.toolbar-arrow-hidden]="toolbarPrevHidden" (click)="toolbarPrev($event)" (mousedown)="stopBubble($event)">

View File

@ -10,12 +10,35 @@ ion-app.app-root core-rich-text-editor {
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 {
padding: 2px;
margin: 2px;
width: 100%;
resize: none;
background-color: $white;
flex-grow: 1;
@include darkmode() {
background-color: $gray-darker;
color: $white;
@ -147,7 +170,6 @@ ion-app.app-root core-rich-text-editor {
border: none;
}
}
}
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 { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreEventsProvider } from '@providers/events';
import { CoreEditorOfflineProvider } from '../../providers/editor-offline';
import { FormControl } from '@angular/forms';
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.
*
* 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.
* If enabled, this component will show a rich text editor. Otherwise it'll show a regular textarea.
*
* Example:
* <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({
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/
// @todo: Anchor button, fullscreen...
// @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() component?: string; // The component to link the files to.
@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>;
@ViewChild('editor') editor: ElementRef; // WYSIWYG 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 editorElement: HTMLDivElement;
protected kbHeight = 0; // Last known keyboard height.
@ -64,7 +67,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
protected valueChangeSubscription: Subscription;
protected keyboardObs: any;
protected initHeightInterval;
protected resetObs: any;
protected initHeightInterval: NodeJS.Timer;
rteEnabled = false;
editorSupported = true;
@ -90,16 +94,31 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
ul: 'false',
ol: 'false',
};
infoMessage: string;
protected isCurrentView = true;
protected toolbarButtonWidth = 40;
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,
private sitesProvider: CoreSitesProvider, private filepoolProvider: CoreFilepoolProvider,
@Optional() private content: Content, elementRef: ElementRef, private events: CoreEventsProvider,
private utils: CoreUtilsProvider, private platform: Platform) {
constructor(
protected domUtils: CoreDomUtilsProvider,
protected urlUtils: CoreUrlUtilsProvider,
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.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.
this.editorElement = this.editor.nativeElement as HTMLDivElement;
this.setContent(this.control.value);
this.originalContent = this.control.value;
this.lastDraft = this.control.value;
this.editorElement.onchange = this.onChange.bind(this);
this.editorElement.onkeyup = 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).
this.valueChangeSubscription = this.control.valueChanges.subscribe((param) => {
this.setContent(param);
if (!this.draftWasRestored || this.originalContent != param) {
// Apply the new content.
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.
@ -148,6 +182,20 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
});
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) {
this.element.style.height = this.domUtils.formatPixelsSize(height);
this.element.style.height = this.domUtils.formatPixelsSize(height - 1);
} else {
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.
*/
hideToolbar($event: any): void {
hideToolbar($event: Event): void {
this.stopBubble($event);
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.
*/
@ -698,5 +874,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
document.removeEventListener('selectionchange', this.updateToolbarStyles);
clearInterval(this.initHeightInterval);
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" class="core-siteurl">{{siteUrl}}</p>
</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-input type="text" name="username" placeholder="{{ 'core.login.username' | translate }}" formControlName="username" autocapitalize="none" autocorrect="off"></ion-input>
</ion-item>

View File

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

View File

@ -17,7 +17,7 @@
<core-loading [hideUntil]="settingsLoaded" *ngIf="!isMinor">
<!-- 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>
<p class="item-heading">{{ 'core.agelocationverification' | translate }}</p>
</ion-item-divider>
@ -47,7 +47,7 @@
</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>
<!-- If no sitename show big siteurl. -->
<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
// 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 { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
@ -35,6 +36,8 @@ import { CoreConfigConstants } from '../../../../configconstants';
})
export class CoreLoginEmailSignupPage {
@ViewChild(Content) content: Content;
@ViewChild('ageForm') ageFormElement: ElementRef;
@ViewChild('signupFormEl') signupFormElement: ElementRef;
signupForm: FormGroup;
siteUrl: string;
@ -66,10 +69,18 @@ export class CoreLoginEmailSignupPage {
policyErrors: any;
namefieldsErrors: any;
constructor(private navCtrl: NavController, navParams: NavParams, private fb: FormBuilder, private wsProvider: CoreWSProvider,
private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider,
private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private utils: CoreUtilsProvider,
private textUtils: CoreTextUtilsProvider, private userProfileFieldDelegate: CoreUserProfileFieldDelegate) {
constructor(protected navCtrl: NavController,
navParams: NavParams,
protected fb: FormBuilder,
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');
@ -265,6 +276,9 @@ export class CoreLoginEmailSignupPage {
return this.wsProvider.callAjax('auth_email_signup_user', params, { siteUrl: this.siteUrl });
}).then((result) => {
if (result.success) {
this.domUtils.triggerFormSubmittedEvent(this.signupFormElement.nativeElement, true);
// Show alert and ho back.
const message = this.translate.instant('core.login.emailconfirmsent', { $a: params.email });
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.
this.wsProvider.callAjax('core_auth_is_minor', params, {siteUrl: this.siteUrl}).then((result) => {
this.domUtils.triggerFormSubmittedEvent(this.ageFormElement.nativeElement, true);
if (!result.status) {
if (this.countryControl.value) {
this.signUpCountryControl.setValue(this.countryControl.value);

View File

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

View File

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

View File

@ -29,7 +29,7 @@
<ion-icon padding name="alert"></ion-icon> {{ 'core.login.reconnectdescription' | translate }}
</p>
</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">
<p>{{username}}</p>
</ion-item>

View File

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

View File

@ -17,7 +17,7 @@
<div text-center padding>
<img src="assets/img/login_logo.png" class="avatar-full login-logo" role="presentation">
</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. -->
<ng-container *ngIf="!fixedSites">
<p padding>{{ 'core.login.newsitedescription' | translate }}</p>

View File

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

View File

@ -1,5 +1,5 @@
<ion-card>
<form #f="ngForm" (ngSubmit)="submitForm($event)" role="search">
<form #f="ngForm" (ngSubmit)="submitForm($event)" role="search" #searchForm>
<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>
<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
// 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 { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils';
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() onClear: EventEmitter<void>; // Send event when clearing the search form.
@ViewChild('searchForm') formElement: ElementRef;
searched = ''; // Last search emitted.
searchText = '';
history: CoreSearchHistoryItem[];
history: CoreSearchHistoryItem[] = [];
historyShown = false;
protected elementClicked = '';
@ -58,6 +63,9 @@ export class CoreSearchBoxComponent implements OnInit, OnDestroy {
constructor(protected translate: TranslateService,
protected utils: CoreUtilsProvider,
protected searchHistoryProvider: CoreSearchHistoryProvider,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider,
protected domUtils: CoreDomUtilsProvider,
) {
this.onSubmit = new EventEmitter<string>();
this.onClear = new EventEmitter<void>();
@ -95,6 +103,8 @@ export class CoreSearchBoxComponent implements OnInit, OnDestroy {
this.saveSearchToHistory(this.searchText);
}
this.domUtils.triggerFormSubmittedEvent(this.formElement.nativeElement, false, this.sitesProvider.getCurrentSiteId());
this.searched = this.searchText;
this.onSubmit.emit(this.searchText);
}

View File

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

View File

@ -22,6 +22,7 @@ import { TranslateService } from '@ngx-translate/core';
import { CoreTextUtilsProvider } from './text';
import { CoreAppProvider } from '../app';
import { CoreConfigProvider } from '../config';
import { CoreEventsProvider } from '../events';
import { CoreLoggerProvider } from '../logger';
import { CoreUrlUtilsProvider } from './url';
import { CoreFileProvider } from '@providers/file';
@ -64,20 +65,21 @@ export class CoreDomUtilsProvider {
protected displayedAlerts = {}; // To prevent duplicated alerts.
protected logger;
constructor(private translate: TranslateService,
private loadingCtrl: LoadingController,
private toastCtrl: ToastController,
private alertCtrl: AlertController,
private textUtils: CoreTextUtilsProvider,
private appProvider: CoreAppProvider,
private platform: Platform,
private configProvider: CoreConfigProvider,
private urlUtils: CoreUrlUtilsProvider,
private modalCtrl: ModalController,
private sanitizer: DomSanitizer,
private popoverCtrl: PopoverController,
private fileProvider: CoreFileProvider,
loggerProvider: CoreLoggerProvider) {
constructor(protected translate: TranslateService,
protected loadingCtrl: LoadingController,
protected toastCtrl: ToastController,
protected alertCtrl: AlertController,
protected textUtils: CoreTextUtilsProvider,
protected appProvider: CoreAppProvider,
protected platform: Platform,
protected configProvider: CoreConfigProvider,
protected urlUtils: CoreUrlUtilsProvider,
protected modalCtrl: ModalController,
protected sanitizer: DomSanitizer,
protected popoverCtrl: PopoverController,
protected fileProvider: CoreFileProvider,
loggerProvider: CoreLoggerProvider,
protected eventsProvider: CoreEventsProvider) {
this.logger = loggerProvider.getInstance('CoreDomUtilsProvider');
@ -1625,4 +1627,32 @@ export class CoreDomUtilsProvider {
// Now move the element into the wrapper.
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;
$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;
$tabs-background: $gray-darker !default;