2
0
Fork 0

MOBILE-3323 editor: Save and restore drafts

main
Dani Palou 2020-01-31 10:08:24 +01:00
parent d2f4df452e
commit 5a79151b01
28 changed files with 228 additions and 36 deletions

View File

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

View File

@ -86,7 +86,7 @@
<!-- Description. --> <!-- Description. -->
<ion-item text-wrap> <ion-item text-wrap>
<ion-label stacked><h2>{{ 'core.description' | translate }}</h2></ion-label> <ion-label stacked><h2>{{ 'core.description' | translate }}</h2></ion-label>
<core-rich-text-editor item-content [control]="descriptionControl" [placeholder]="'core.description' | translate" name="description" [component]="component" [componentId]="eventId"></core-rich-text-editor> <core-rich-text-editor item-content [control]="descriptionControl" [placeholder]="'core.description' | translate" name="description" [component]="component" [componentId]="eventId" [autoSave]="false"></core-rich-text-editor>
</ion-item> </ion-item>
<!-- Location. --> <!-- Location. -->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,7 +57,7 @@
<!-- Essay. --> <!-- Essay. -->
<ng-container *ngSwitchCase="'essay'"> <ng-container *ngSwitchCase="'essay'">
<ion-item *ngIf="question.textarea"> <ion-item *ngIf="question.textarea">
<core-rich-text-editor item-content placeholder="{{ 'addon.mod_lesson.youranswer' | translate }}" [control]="question.control" [component]="component" [componentId]="lesson.coursemodule"></core-rich-text-editor> <core-rich-text-editor item-content placeholder="{{ 'addon.mod_lesson.youranswer' | translate }}" [control]="question.control" [component]="component" [componentId]="lesson.coursemodule" [autoSave]="true" contextLevel="module" [contextInstanceId]="lesson.coursemodule" elementId="answer_editor"></core-rich-text-editor>
</ion-item> </ion-item>
<ion-item text-wrap *ngIf="!question.textarea && question.useranswer"> <ion-item text-wrap *ngIf="!question.textarea && question.useranswer">
<p class="item-heading">{{ 'addon.mod_lesson.youranswer' | translate }}</p> <p class="item-heading">{{ 'addon.mod_lesson.youranswer' | translate }}</p>

View File

@ -17,7 +17,7 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<core-rich-text-editor item-content [control]="contentControl" [placeholder]="'core.content' | translate" name="wiki_page_content" [component]="component" [componentId]="componentId"></core-rich-text-editor> <core-rich-text-editor item-content [control]="contentControl" [placeholder]="'core.content' | translate" name="wiki_page_content" [component]="component" [componentId]="componentId" [autoSave]="true" contextLevel="module" [contextInstanceId]="module.id || 0" elementId="newcontent_editor" [draftExtraParams]="editorExtraParams"></core-rich-text-editor>
</ion-item> </ion-item>
<ion-item *ngIf="wrongVersionLock" text-center class="addon-mod_wiki-wrongversionlock" > <ion-item *ngIf="wrongVersionLock" text-center class="addon-mod_wiki-wrongversionlock" >

View File

@ -45,6 +45,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy {
component = AddonModWikiProvider.COMPONENT; // Component to link the files to. component = AddonModWikiProvider.COMPONENT; // Component to link the files to.
componentId: number; // Component ID to link the files to. componentId: number; // Component ID to link the files to.
wrongVersionLock: boolean; // Whether the page lock doesn't match the initial one. wrongVersionLock: boolean; // Whether the page lock doesn't match the initial one.
editorExtraParams: {[name: string]: any} = {};
protected module: any; // Wiki module instance. protected module: any; // Wiki module instance.
protected courseId: number; // Course the wiki belongs to. protected courseId: number; // Course the wiki belongs to.
@ -101,6 +102,20 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy {
// Block the wiki so it cannot be synced. // Block the wiki so it cannot be synced.
this.syncProvider.blockOperation(this.component, this.blockId); this.syncProvider.blockOperation(this.component, this.blockId);
if (!this.module.id) {
this.editorExtraParams.type = 'wiki';
}
if (this.pageId) {
this.editorExtraParams.pageid = this.pageId;
if (this.section) {
this.editorExtraParams.section = this.section;
}
} else if (pageTitle) {
this.editorExtraParams.pagetitle = pageTitle;
}
} }
/** /**

View File

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

View File

@ -60,7 +60,7 @@
</ion-item> </ion-item>
<ion-item *ngIf="access.canoverridegrades"> <ion-item *ngIf="access.canoverridegrades">
<ion-label stacked>{{ 'addon.mod_workshop.feedbackreviewer' | translate }}</ion-label> <ion-label stacked>{{ 'addon.mod_workshop.feedbackreviewer' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="evaluateForm.controls['text']" formControlName="text"></core-rich-text-editor> <core-rich-text-editor item-content [control]="evaluateForm.controls['text']" formControlName="text" [autoSave]="true" contextLevel="module" [contextInstanceId]="workshop.coursemodule" elementId="feedbackreviewer_editor" [draftExtraParams]="{asid: assessmentId}"></core-rich-text-editor>
</ion-item> </ion-item>
</form> </form>
<ion-list *ngIf="!evaluating && evaluate && evaluate.text"> <ion-list *ngIf="!evaluating && evaluate && evaluate.text">

View File

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

View File

@ -51,6 +51,7 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
component = AddonModWorkshopProvider.COMPONENT; component = AddonModWorkshopProvider.COMPONENT;
componentId: number; componentId: number;
editForm: FormGroup; // The form group. editForm: FormGroup; // The form group.
editorExtraParams: {[name: string]: any} = {};
protected workshopId: number; protected workshopId: number;
protected submissionId: number; protected submissionId: number;
@ -86,6 +87,10 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
this.editForm = new FormGroup({}); this.editForm = new FormGroup({});
this.editForm.addControl('title', this.fb.control('', Validators.required)); this.editForm.addControl('title', this.fb.control('', Validators.required));
this.editForm.addControl('content', this.fb.control('')); this.editForm.addControl('content', this.fb.control(''));
if (this.submissionId) {
this.editorExtraParams.id = this.submissionId;
}
} }
/** /**

View File

@ -87,7 +87,7 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-label stacked>{{ 'addon.mod_workshop.feedbackauthor' | translate }}</ion-label> <ion-label stacked>{{ 'addon.mod_workshop.feedbackauthor' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="feedbackForm.controls['text']" formControlName="text"></core-rich-text-editor> <core-rich-text-editor item-content [control]="feedbackForm.controls['text']" formControlName="text" [autoSave]="true" contextLevel="module" [contextInstanceId]="module.id" elementId="feedbackauthor_editor" [draftExtraParams]="{id: submissionId}"></core-rich-text-editor>
</ion-item> </ion-item>
</form> </form>

View File

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

View File

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

View File

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

View File

@ -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 class="core-rte-editor-container">
</div> <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"> <div #toolbar class="core-rte-toolbar" [class.toolbar-hidden]="toolbarHidden">
<button *ngIf="toolbarArrows" class="toolbar-arrow" [class.toolbar-arrow-hidden]="toolbarPrevHidden" (click)="toolbarPrev($event)" (mousedown)="stopBubble($event)"> <button *ngIf="toolbarArrows" class="toolbar-arrow" [class.toolbar-arrow-hidden]="toolbarPrevHidden" (click)="toolbarPrev($event)" (mousedown)="stopBubble($event)">

View File

@ -10,6 +10,23 @@ ion-app.app-root core-rich-text-editor {
background-color: $gray-darker; background-color: $gray-darker;
} }
.core-rte-editor-container {
height: 100%;
position: relative;
.core-rte-info-message {
position: absolute;
bottom: 0;
padding: 3px;
border: 1px solid $info;
border-radius: 5px;
.icon {
color: $info;
}
}
}
.core-rte-editor, .core-textarea { .core-rte-editor, .core-textarea {
padding: 2px; padding: 2px;
margin: 2px; margin: 2px;
@ -147,7 +164,6 @@ ion-app.app-root core-rich-text-editor {
border: none; border: none;
} }
} }
} }
body.keyboard-is-open ion-app.app-root core-rich-text-editor { body.keyboard-is-open ion-app.app-root core-rich-text-editor {

View File

@ -21,6 +21,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreEditorOfflineProvider } from '../../providers/editor-offline';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
@ -46,11 +47,19 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe
@Input() name = 'core-rich-text-editor'; // Name to set to the textarea. @Input() name = 'core-rich-text-editor'; // Name to set to the textarea.
@Input() component?: string; // The component to link the files to. @Input() component?: string; // The component to link the files to.
@Input() componentId?: number; // An ID to use in conjunction with the component. @Input() componentId?: number; // An ID to use in conjunction with the component.
@Input() autoSave?: boolean | string; // Whether to auto-save the contents in a draft. Defaults to true.
@Input() contextLevel?: string; // The context level of the text.
@Input() contextInstanceId?: number; // The instance ID related to the context.
@Input() elementId?: string; // An ID to set to the element.
@Input() draftExtraParams: {[name: string]: any}; // Extra params to identify the draft.
@Output() contentChanged: EventEmitter<string>; @Output() contentChanged: EventEmitter<string>;
@ViewChild('editor') editor: ElementRef; // WYSIWYG editor. @ViewChild('editor') editor: ElementRef; // WYSIWYG editor.
@ViewChild('textarea') textarea: TextInput; // Textarea editor. @ViewChild('textarea') textarea: TextInput; // Textarea editor.
protected DRAFT_AUTOSAVE_FREQUENCY = 30000;
protected RESTORE_MESSAGE_CLEAR_TIME = 6000;
protected SAVE_MESSAGE_CLEAR_TIME = 2000;
protected element: HTMLDivElement; protected element: HTMLDivElement;
protected editorElement: HTMLDivElement; protected editorElement: HTMLDivElement;
protected kbHeight = 0; // Last known keyboard height. protected kbHeight = 0; // Last known keyboard height.
@ -84,16 +93,30 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe
ul: 'false', ul: 'false',
ol: 'false', ol: 'false',
}; };
infoMessage: string;
protected isCurrentView = true; protected isCurrentView = true;
protected toolbarButtonWidth = 40; protected toolbarButtonWidth = 40;
protected toolbarArrowWidth = 28; protected toolbarArrowWidth = 28;
protected pageInstance: string;
protected autoSaveInterval: NodeJS.Timer;
protected hideMessageTimeout: NodeJS.Timer;
protected lastDraft = '';
protected draftWasRestored = false;
constructor(private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, constructor(
private sitesProvider: CoreSitesProvider, private filepoolProvider: CoreFilepoolProvider, protected domUtils: CoreDomUtilsProvider,
@Optional() private content: Content, elementRef: ElementRef, private events: CoreEventsProvider, protected urlUtils: CoreUrlUtilsProvider,
private utils: CoreUtilsProvider, private platform: Platform) { protected sitesProvider: CoreSitesProvider,
protected filepoolProvider: CoreFilepoolProvider,
@Optional() protected content: Content,
elementRef: ElementRef,
protected events: CoreEventsProvider,
protected utils: CoreUtilsProvider,
protected platform: Platform,
protected editorOffline: CoreEditorOfflineProvider) {
this.contentChanged = new EventEmitter<string>(); this.contentChanged = new EventEmitter<string>();
this.element = elementRef.nativeElement as HTMLDivElement; this.element = elementRef.nativeElement as HTMLDivElement;
this.pageInstance = 'app_' + Date.now(); // Generate a "unique" ID based on timestamp.
} }
/** /**
@ -117,7 +140,9 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe
// Listen for changes on the control to update the editor (if it is updated from outside of this component). // Listen for changes on the control to update the editor (if it is updated from outside of this component).
this.valueChangeSubscription = this.control.valueChanges.subscribe((param) => { this.valueChangeSubscription = this.control.valueChanges.subscribe((param) => {
this.setContent(param); if (!this.draftWasRestored) {
this.setContent(param);
}
}); });
// Use paragraph on enter. // Use paragraph on enter.
@ -142,6 +167,20 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe
}); });
this.updateToolbarButtons(); this.updateToolbarButtons();
if (this.elementId) {
// Prepend elementId with 'id_' like in web. Don't use a setter for this because the value shouldn't change.
this.elementId = 'id_' + this.elementId;
this.element.setAttribute('id', this.elementId);
}
if (this.shouldAutoSaveDrafts()) {
// Recover drafts.
this.restoreDraft();
// Auto save drafts every certain time.
this.autoSaveDrafts();
}
} }
/** /**
@ -667,6 +706,97 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe
} }
} }
/**
* 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 {
let draftText = await this.editorOffline.resumeDraft(this.contextLevel, this.contextInstanceId, this.elementId,
this.draftExtraParams, this.pageInstance);
if (typeof draftText == 'undefined') {
// No draft found.
return;
}
// 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;
// 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);
// 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);
}
/**
* Show a message.
*
* @param message Identifier of the message to display.
* @param timeout Number of milliseconds when to remove the message.
*/
protected showMessage(message: string, timeout: number): void {
clearTimeout(this.hideMessageTimeout);
this.infoMessage = message;
this.hideMessageTimeout = setTimeout(() => {
this.hideMessageTimeout = null;
this.infoMessage = null;
}, timeout);
}
/** /**
* User entered the page that contains the component. * User entered the page that contains the component.
*/ */
@ -692,5 +822,7 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe
document.removeEventListener('selectionchange', this.updateToolbarStyles); document.removeEventListener('selectionchange', this.updateToolbarStyles);
clearInterval(this.initHeightInterval); clearInterval(this.initHeightInterval);
this.keyboardObs && this.keyboardObs.off(); this.keyboardObs && this.keyboardObs.off();
clearInterval(this.autoSaveInterval);
clearTimeout(this.hideMessageTimeout);
} }
} }

View File

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

View File

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