MOBILE-3323 editor: Save and restore drafts
parent
d2f4df452e
commit
5a79151b01
|
@ -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",
|
||||
|
|
|
@ -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. -->
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" >
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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. -->
|
||||
|
|
|
@ -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>
|
|
@ -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.",
|
||||
|
|
|
@ -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)">
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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> </p>' || draftText == '<p><br> </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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"autosavesucceeded": "Draft saved.",
|
||||
"textrecovered": "A draft version of this text was automatically restored."
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue