MOBILE-3592 editor: Migrate rich text editor

main
Dani Palou 2020-12-03 16:01:02 +01:00
parent fd0ea51096
commit 90a0f18480
14 changed files with 1803 additions and 12 deletions

View File

@ -14,7 +14,7 @@
<span [core-mark-required]="required">{{ field.name }}</span>
<core-input-errors [control]="control"></core-input-errors>
</ion-label>
<!-- @todo <core-rich-text-editor item-content [control]="control" [placeholder]="field.name" [autoSave]="true"
<core-rich-text-editor item-content [control]="control" [placeholder]="field.name" [autoSave]="true"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [elementId]="modelName">
</core-rich-text-editor> -->
</core-rich-text-editor>
</ion-item>

View File

@ -23,7 +23,7 @@ import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profi
import { AddonUserProfileFieldTextareaComponent } from './component/textarea';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
// @todo import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
@NgModule({
declarations: [
@ -37,7 +37,7 @@ import { CoreDirectivesModule } from '@directives/directives.module';
ReactiveFormsModule,
CoreComponentsModule,
CoreDirectivesModule,
// CoreEditorComponentsModule,
CoreEditorComponentsModule,
],
providers: [
{

View File

@ -6,7 +6,7 @@
<ion-icon *ngIf="showPrevButton" name="fas-chevron-left"></ion-icon>
</ion-col>
<ion-col class="ion-no-padding" size="10">
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slideOpts" [dir]="direction" role="tablist"
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slidesOpts" [dir]="direction" role="tablist"
[attr.aria-label]="description" aria-hidden="false">
<ng-container *ngFor="let tab of tabs">
<ion-slide [hidden]="!hideUntil" [attr.aria-selected]="selected == tab.id" class="tab-slide"

View File

@ -82,7 +82,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
direction = 'ltr';
description = '';
lastScroll = 0;
slideOpts = {
slidesOpts = {
initialSlide: 0,
slidesPerView: 3,
centerInsufficientSlides: true,
@ -381,13 +381,12 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
protected async updateSlides(): Promise<void> {
this.numTabsShown = this.tabs.reduce((prev: number, current: CoreTab) => current.enabled ? prev + 1 : prev, 0);
this.slideOpts.slidesPerView = Math.min(this.maxSlides, this.numTabsShown);
this.slidesSwiper.params.slidesPerView = this.slideOpts.slidesPerView;
this.slidesOpts = { ...this.slidesOpts, slidesPerView: Math.min(this.maxSlides, this.numTabsShown) };
this.calculateTabBarHeight();
await this.slides!.update();
if (!this.hasSliddenToInitial && this.selectedIndex && this.selectedIndex >= this.slideOpts.slidesPerView) {
if (!this.hasSliddenToInitial && this.selectedIndex && this.selectedIndex >= this.slidesOpts.slidesPerView) {
this.hasSliddenToInitial = true;
this.shouldSlideToInitial = true;
@ -637,6 +636,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
window.removeEventListener('resize', this.resizeFunction);
}
this.stackEventsSubscription?.unsubscribe();
this.languageChangedSubscription.unsubscribe();
}
}

View File

@ -0,0 +1,42 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreEditorRichTextEditorComponent } from './rich-text-editor/rich-text-editor';
import { CoreComponentsModule } from '@components/components.module';
@NgModule({
declarations: [
CoreEditorRichTextEditorComponent,
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
],
providers: [
],
exports: [
CoreEditorRichTextEditorComponent,
],
entryComponents: [
CoreEditorRichTextEditorComponent,
],
})
export class CoreEditorComponentsModule {}

View File

@ -0,0 +1,113 @@
<div class="core-rte-editor-container" (click)="focusRTE()" [class.toolbar-hidden]="toolbarHidden">
<div [hidden]="!rteEnabled" #editor contenteditable="true" class="core-rte-editor" button (focus)="showToolbar($event)"
(longPress)="showToolbar($event)" (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()" (focus)="showToolbar($event)" (longPress)="showToolbar($event)"
(blur)="hideToolbar($event)" role="textbox">
</ion-textarea>
<div class="core-rte-info-message" *ngIf="infoMessage">
<ion-icon name="fas-info-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)="mouseDownAction($event)">
<ion-icon name="fas-chevron-left"></ion-icon>
</button>
<ion-slides [options]="slidesOpts" [dir]="direction" (ionSlideDidChange)="updateToolbarArrows()">
<!-- https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand -->
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.strong" [title]="'core.editor.bold' | translate"
(click)="buttonAction($event, 'bold', 'strong')" (mousedown)="mouseDownAction($event)">
<core-icon name="fas-bold"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.em" (click)="buttonAction($event, 'italic', 'em')"
(mousedown)="mouseDownAction($event)" [title]=" 'core.editor.italic' | translate">
<core-icon name="fas-italic"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.u" (click)="buttonAction($event, 'underline', 'u')"
(mousedown)="mouseDownAction($event)" [title]="'core.editor.underline' | translate">
<core-icon name="fas-underline"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.strike" [title]="'core.editor.strike' | translate"
(click)="buttonAction($event, 'strikethrough', 'strike')" (mousedown)="mouseDownAction($event)">
<core-icon name="fas-strikethrough"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.p" (click)="buttonAction($event, 'p', 'block')"
(mousedown)="mouseDownAction($event)" [title]="'core.editor.p' | translate">
<core-icon name="fas-paragraph"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h3" (click)="buttonAction($event, 'h3', 'block')"
(mousedown)="mouseDownAction($event)" [title]="'core.editor.h3' | translate">
<core-icon name="fas-heading"></core-icon>3
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h4" (click)="buttonAction($event, 'h4', 'block')"
(mousedown)="mouseDownAction($event)" [title]="'core.editor.h4' | translate">
<core-icon name="fas-heading"></core-icon>4
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h5" (click)="buttonAction($event, 'h5', 'block')"
(mousedown)="mouseDownAction($event)" [title]="'core.editor.h5' | translate">
<core-icon name="fas-heading"></core-icon>5
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.ul" (mousedown)="mouseDownAction($event)"
(click)="buttonAction($event, 'insertUnorderedList')" [title]="'core.editor.unorderedlist' | translate">
<core-icon name="fas-list-ul"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.ol" (mousedown)="mouseDownAction($event)"
(click)="buttonAction($event, 'insertOrderedList')" [title]="'core.editor.orderedlist' | translate">
<core-icon name="fas-list-ol"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" (click)="buttonAction($event, 'removeFormat')" (mousedown)="mouseDownAction($event)"
[title]="'core.editor.clear' | translate">
<core-icon name="fas-eraser"></core-icon>
</button>
</ion-slide>
<ion-slide *ngIf="canScanQR">
<button [disabled]="!rteEnabled" (click)="scanQR($event)" (mousedown)="stopBubble($event)"
[title]="'core.scanqr' | translate">
<core-icon name="fas-qrcode"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [attr.aria-pressed]="!rteEnabled" (click)="toggleEditor($event)" (mousedown)="mouseDownAction($event)"
[title]=" 'core.editor.toggle' | translate">
<core-icon name="fas-code"></core-icon>
</button>
</ion-slide>
<ion-slide *ngIf="isPhone">
<button (click)="hideToolbar($event)" (mousedown)="mouseDownAction($event)"
[title]=" 'core.editor.hidetoolbar' | translate">
<core-icon name="fas-times"></core-icon>
</button>
</ion-slide>
</ion-slides>
<button *ngIf="toolbarArrows" class="toolbar-arrow" [class.toolbar-arrow-hidden]="toolbarNextHidden"
(click)="toolbarNext($event)" (mousedown)="mouseDownAction($event)">
<ion-icon name="fas-chevron-right"></ion-icon>
</button>
</div>

View File

@ -0,0 +1,181 @@
:host {
height: 40vh;
overflow: hidden;
min-height: 200px; /* Just in case vh is not supported */
min-height: 40vh;
width: 100%;
display: flex;
flex-direction: column;
// @include darkmode() {
// background-color: $gray-darker;
// }
.core-rte-editor-container {
max-height: calc(100% - 46px);
display: flex;
flex-direction: column;
flex-grow: 1;
&.toolbar-hidden {
max-height: 100%;
}
.core-rte-info-message {
padding: 5px;
border-top: 1px solid var(--ion-color-secondary);
background: white;
flex-shrink: 1;
font-size: 1.4rem;
.icon {
color: var(--ion-color-secondary);
}
}
}
.core-rte-editor, .core-textarea {
padding: 2px;
margin: 2px;
width: 100%;
resize: none;
background-color: white;
flex-grow: 1;
// @include darkmode() {
// background-color: var(--gray-darker);
// color: var(--white);
// }
}
.core-rte-editor {
flex-grow: 1;
flex-shrink: 1;
-webkit-user-select: auto !important;
word-wrap: break-word;
overflow-x: hidden;
overflow-y: auto;
cursor: text;
img {
// @include padding(null, null, null, 2px);
max-width: 95%;
width: auto;
}
&:empty:before {
content: attr(data-placeholder-text);
display: block;
color: var(--gray-light);
font-weight: bold;
// @include darkmode() {
// color: $gray;
// }
}
// Make empty elements selectable (to move the cursor).
*:empty:after {
content: '\200B';
}
}
.core-textarea {
flex-grow: 1;
flex-shrink: 1;
position: relative;
textarea {
margin: 0 !important;
padding: 0;
height: 100% !important;
width: 100% !important;
resize: none;
overflow-x: hidden;
overflow-y: auto;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
}
div.core-rte-toolbar {
display: flex;
width: 100%;
z-index: 1;
flex-grow: 0;
flex-shrink: 0;
background-color: var(--white);
// @include darkmode() {
// background-color: $black;
// }
// @include padding(5px, null);
border-top: 1px solid var(--gray);
ion-slides {
width: 240px;
flex-grow: 1;
flex-shrink: 1;
}
button {
display: flex;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
padding-right: 6px;
padding-left: 6px;
margin: 0 auto;
font-size: 18px;
background-color: var(--white);
border-radius: 4px;
// @include core-transition(background-color, 200ms);
color: var(--ion-text-color);
cursor: pointer;
// @include darkmode() {
// background-color: $black;
// color: $core-dark-text-color;
// }
&.toolbar-button-enable {
width: 100%;
}
&:active, &[aria-pressed="true"] {
background-color: var(--gray);
// @include darkmode() {
// background-color: $gray-dark;
// }
}
&.toolbar-arrow {
width: 28px;
flex-grow: 0;
flex-shrink: 0;
opacity: 1;
// @include core-transition(opacity, 200ms);
&:active {
background-color: var(--white);
// @include darkmode() {
// background-color: $black;
// }
}
&.toolbar-arrow-hidden {
opacity: 0;
}
}
}
&.toolbar-hidden {
visibility: none;
height: 0;
border: none;
}
}
}
:host-context(.keyboard-is-open) {
min-height: 200px;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CORE_SITE_SCHEMAS } from '@services/sites';
import { CoreEditorComponentsModule } from './components/components.module';
import { SITE_SCHEMA } from './services/database/editor';
@NgModule({
declarations: [
],
imports: [
CoreEditorComponentsModule,
],
providers: [
{
provide: CORE_SITE_SCHEMAS,
useValue: [SITE_SCHEMA],
multi: true,
},
],
})
export class CoreEditorModule {}

View File

@ -0,0 +1,17 @@
{
"autosavesucceeded": "Draft saved.",
"bold": "Bold",
"clear": "Clear formatting",
"h3": "Heading (large)",
"h4": "Heading (medium)",
"h5": "Heading (small)",
"hidetoolbar": "Hide toolbar",
"italic": "Italic",
"orderedlist": "Ordered list",
"p": "Paragraph",
"strike": "Strike through",
"textrecovered": "A draft version of this text was automatically restored.",
"toggle": "Toggle editor",
"underline": "Underline",
"unorderedlist": "Unordered list"
}

View File

@ -0,0 +1,93 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreSiteSchema } from '@services/sites';
/**
* Database variables for CoreEditorOffline service.
*/
export const DRAFT_TABLE = 'editor_draft';
export const SITE_SCHEMA: CoreSiteSchema = {
name: 'CoreEditorProvider',
version: 1,
tables: [
{
name: DRAFT_TABLE,
columns: [
{
name: 'contextlevel',
type: 'TEXT',
},
{
name: 'contextinstanceid',
type: 'INTEGER',
},
{
name: 'elementid',
type: 'TEXT',
},
{
name: 'extraparams', // Moodle web uses a page hash built with URL. App will use some params stringified.
type: 'TEXT',
},
{
name: 'drafttext',
type: 'TEXT',
notNull: true,
},
{
name: 'pageinstance',
type: 'TEXT',
notNull: true,
},
{
name: 'timecreated',
type: 'INTEGER',
notNull: true,
},
{
name: 'timemodified',
type: 'INTEGER',
notNull: true,
},
{
name: 'originalcontent',
type: 'TEXT',
},
],
primaryKeys: ['contextlevel', 'contextinstanceid', 'elementid', 'extraparams'],
},
],
};
/**
* Primary data to identify a stored draft.
*/
export type CoreEditorDraftPrimaryData = {
contextlevel: string; // Context level.
contextinstanceid: number; // The instance ID related to the context.
elementid: string; // Element ID.
extraparams: string; // Extra params stringified.
};
/**
* Draft data stored.
*/
export type CoreEditorDraft = CoreEditorDraftPrimaryData & {
drafttext?: string; // Draft text stored.
pageinstance?: string; // Unique identifier to prevent storing data from several sources at the same time.
timecreated?: number; // Time created.
timemodified?: number; // Time modified.
originalcontent?: string; // Original content of the editor.
};

View File

@ -0,0 +1,239 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreError } from '@classes/errors/error';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { CoreLogger } from '@singletons/logger';
import { CoreEditorDraft, CoreEditorDraftPrimaryData, DRAFT_TABLE } from './database/editor';
/**
* Service with features regarding rich text editor in offline.
*/
@Injectable({ providedIn: 'root' })
export class CoreEditorOfflineProvider {
protected logger: CoreLogger;
constructor() {
this.logger = CoreLogger.getInstance('CoreEditorOfflineProvider');
}
/**
* Delete a draft from DB.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteDraft(
contextLevel: string,
contextInstanceId: number,
elementId: string,
extraParams: Record<string, unknown>,
siteId?: string,
): Promise<void> {
try {
const db = await CoreSites.instance.getSiteDb(siteId);
const params = this.fixDraftPrimaryData(contextLevel, contextInstanceId, elementId, extraParams);
await db.deleteRecords(DRAFT_TABLE, params);
} catch (error) {
// Ignore errors, probably no draft stored.
}
}
/**
* Return an object with the draft primary data converted to the right format.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @return Object with the fixed primary data.
*/
protected fixDraftPrimaryData(
contextLevel: string,
contextInstanceId: number,
elementId: string,
extraParams: Record<string, unknown>,
): CoreEditorDraftPrimaryData {
return {
contextlevel: contextLevel,
contextinstanceid: contextInstanceId,
elementid: elementId,
extraparams: CoreUtils.instance.sortAndStringify(extraParams || {}),
};
}
/**
* Get a draft from DB.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the draft data. Undefined if no draft stored.
*/
async getDraft(
contextLevel: string,
contextInstanceId: number,
elementId: string,
extraParams: Record<string, unknown>,
siteId?: string,
): Promise<CoreEditorDraft> {
const db = await CoreSites.instance.getSiteDb(siteId);
const params = this.fixDraftPrimaryData(contextLevel, contextInstanceId, elementId, extraParams);
return db.getRecord(DRAFT_TABLE, params);
}
/**
* Get draft to resume it.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @param pageInstance Unique identifier to prevent storing data from several sources at the same time.
* @param originalContent Original content of the editor.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the draft data. Undefined if no draft stored.
*/
async resumeDraft(
contextLevel: string,
contextInstanceId: number,
elementId: string,
extraParams: Record<string, unknown>,
pageInstance: string,
originalContent?: string,
siteId?: string,
): Promise<CoreEditorDraft | undefined> {
try {
// Check if there is a draft stored.
const entry = await this.getDraft(contextLevel, contextInstanceId, elementId, extraParams, siteId);
// There is a draft stored. Update its page instance.
try {
const db = await CoreSites.instance.getSiteDb(siteId);
entry.pageinstance = pageInstance;
entry.timemodified = Date.now();
if (originalContent && entry.originalcontent != originalContent) {
entry.originalcontent = originalContent;
entry.drafttext = ''; // "Discard" the draft.
}
await db.insertRecord(DRAFT_TABLE, entry);
} catch (error) {
// Ignore errors saving the draft. It shouldn't happen.
}
return entry;
} catch (error) {
// No draft stored. Store an empty draft to save the pageinstance.
await this.saveDraft(
contextLevel,
contextInstanceId,
elementId,
extraParams,
pageInstance,
'',
originalContent,
siteId,
);
}
}
/**
* Save a draft in DB.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @param pageInstance Unique identifier to prevent storing data from several sources at the same time.
* @param draftText The text to store.
* @param originalContent Original content of the editor.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async saveDraft(
contextLevel: string,
contextInstanceId: number,
elementId: string,
extraParams: Record<string, unknown>,
pageInstance: string,
draftText: string,
originalContent?: string,
siteId?: string,
): Promise<void> {
let timecreated = Date.now();
let entry: CoreEditorDraft | undefined;
// Check if there is a draft already stored.
try {
entry = await this.getDraft(contextLevel, contextInstanceId, elementId, extraParams, siteId);
timecreated = entry.timecreated || timecreated;
} catch (error) {
// No draft already stored.
}
if (entry) {
if (entry.pageinstance != pageInstance) {
this.logger.warn(`Discarding draft because of pageinstance. Context '${contextLevel}' '${contextInstanceId}', ` +
`element '${elementId}'`);
throw new CoreError('Draft was discarded because it was modified in another page.');
}
if (!originalContent) {
// Original content not set, use the one in the entry.
originalContent = entry.originalcontent;
}
}
const db = await CoreSites.instance.getSiteDb(siteId);
const data: CoreEditorDraft = this.fixDraftPrimaryData(contextLevel, contextInstanceId, elementId, extraParams);
data.drafttext = (draftText || '').trim();
data.pageinstance = pageInstance;
data.timecreated = timecreated;
data.timemodified = Date.now();
if (originalContent) {
data.originalcontent = originalContent;
}
await db.insertRecord(DRAFT_TABLE, data);
}
}
export class CoreEditorOffline extends makeSingleton(CoreEditorOfflineProvider) {}

View File

@ -20,7 +20,7 @@ import { Md5 } from 'ts-md5';
import { CoreApp } from '@services/app';
import { CoreConfig } from '@services/config';
import { CoreEvents } from '@singletons/events';
import { CoreEventFormAction, CoreEvents } from '@singletons/events';
import { CoreFile } from '@services/file';
import { CoreWSExternalWarning } from '@services/ws';
import { CoreTextUtils, CoreTextErrorObject } from '@services/utils/text';
@ -1717,7 +1717,7 @@ export class CoreDomUtilsProvider {
}
CoreEvents.trigger(CoreEvents.FORM_ACTION, {
action: 'cancel',
action: CoreEventFormAction.CANCEL,
form: formRef.nativeElement,
}, siteId);
}
@ -1735,7 +1735,7 @@ export class CoreDomUtilsProvider {
}
CoreEvents.trigger(CoreEvents.FORM_ACTION, {
action: 'submit',
action: CoreEventFormAction.SUBMIT,
form: formRef.nativeElement || formRef,
online: !!online,
}, siteId);

View File

@ -249,3 +249,17 @@ export type CoreEventUserDeletedData = CoreEventSiteData & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params: any; // Params sent to the WS that failed.
};
export enum CoreEventFormAction {
CANCEL = 'cancel',
SUBMIT = 'submit',
}
/**
* Data passed to FORM_ACTION event.
*/
export type CoreEventFormActionData = CoreEventSiteData & {
action: CoreEventFormAction; // Action performed.
form: HTMLElement; // Form element.
online?: boolean; // Whether the data was sent to server or not. Only when submitting.
};