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.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

@ -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

@ -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

@ -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

@ -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

@ -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,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

@ -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

@ -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

@ -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

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

@ -45,6 +45,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 +102,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;
}
}
/**

View File

@ -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

@ -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

@ -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

@ -51,6 +51,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 +87,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;
}
}
/**

View File

@ -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

@ -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

@ -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

@ -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

@ -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">
<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,6 +10,23 @@ ion-app.app-root core-rich-text-editor {
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 {
padding: 2px;
margin: 2px;
@ -147,7 +164,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,6 +21,7 @@ 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';
@ -46,11 +47,19 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe
@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.
@ -84,16 +93,30 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe
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;
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.
}
/**
@ -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).
this.valueChangeSubscription = this.control.valueChanges.subscribe((param) => {
this.setContent(param);
if (!this.draftWasRestored) {
this.setContent(param);
}
});
// Use paragraph on enter.
@ -142,6 +167,20 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe
});
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.
*/
@ -692,5 +822,7 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe
document.removeEventListener('selectionchange', this.updateToolbarStyles);
clearInterval(this.initHeightInterval);
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;
$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;