forked from EVOgeek/Vmeda.Online
MOBILE-3323 editor: Save and restore drafts
parent
d2f4df452e
commit
5a79151b01
|
@ -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",
|
||||||
|
|
|
@ -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. -->
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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" >
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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. -->
|
||||||
|
|
|
@ -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>
|
|
@ -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.",
|
||||||
|
|
|
@ -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)">
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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> </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.
|
* 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
$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;
|
||||||
|
|
Loading…
Reference in New Issue