From d2f4df452e56cd02db42df3681a52e65aa86cd20 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 28 Jan 2020 17:57:41 +0100 Subject: [PATCH] MOBILE-3323 editor: Create offline provider for editor --- src/core/editor/editor.module.ts | 17 +- src/core/editor/providers/editor-offline.ts | 254 ++++++++++++++++++++ 2 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 src/core/editor/providers/editor-offline.ts diff --git a/src/core/editor/editor.module.ts b/src/core/editor/editor.module.ts index 1052cfb8b..e55c88a49 100644 --- a/src/core/editor/editor.module.ts +++ b/src/core/editor/editor.module.ts @@ -14,14 +14,25 @@ import { NgModule } from '@angular/core'; import { CoreEditorComponentsModule } from './components/components.module'; +import { CoreEditorOfflineProvider } from './providers/editor-offline'; + +// List of providers (without handlers). +export const CORE_GRADES_PROVIDERS: any[] = [ + CoreEditorOfflineProvider, +]; @NgModule({ declarations: [ ], imports: [ - CoreEditorComponentsModule + CoreEditorComponentsModule, ], providers: [ - ] + CoreEditorOfflineProvider, + ], }) -export class CoreEditorModule {} +export class CoreEditorModule { + constructor(editorOffline: CoreEditorOfflineProvider) { + // Inject the helper even if it isn't used here it's instantiated. + } +} diff --git a/src/core/editor/providers/editor-offline.ts b/src/core/editor/providers/editor-offline.ts new file mode 100644 index 000000000..0f0b5e28c --- /dev/null +++ b/src/core/editor/providers/editor-offline.ts @@ -0,0 +1,254 @@ +// (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 { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; + +/** + * Service with features regarding rich text editor in offline. + */ +@Injectable() +export class CoreEditorOfflineProvider { + + protected DRAFT_TABLE = 'editor_draft'; + + protected logger; + protected siteSchema: CoreSiteSchema = { + name: 'CoreEditorProvider', + version: 1, + tables: [ + { + name: this.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 + }, + ], + primaryKeys: ['contextlevel', 'contextinstanceid', 'elementid', 'extraparams'] + }, + ], + }; + + constructor( + logger: CoreLoggerProvider, + protected sitesProvider: CoreSitesProvider, + protected textUtils: CoreTextUtilsProvider, + protected utils: CoreUtilsProvider) { + this.logger = logger.getInstance('CoreEditorProvider'); + + this.sitesProvider.registerSiteSchema(this.siteSchema); + } + + /** + * 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: {[name: string]: any}, + siteId?: string): Promise { + + try { + const db = await this.sitesProvider.getSiteDb(siteId); + + const params = this.fixDraftPrimaryData(contextLevel, contextInstanceId, elementId, extraParams); + + return db.deleteRecords(this.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: {[name: string]: any}): CoreEditorDraftPrimaryData { + + return { + contextlevel: contextLevel, + contextinstanceid: contextInstanceId, + elementid: elementId, + extraparams: this.utils.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: {[name: string]: any}, + siteId?: string): Promise { + + const db = await this.sitesProvider.getSiteDb(siteId); + + const params = this.fixDraftPrimaryData(contextLevel, contextInstanceId, elementId, extraParams); + + return db.getRecord(this.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 siteId Site ID. If not defined, current site. + * @return Promise resolved with the draft text. Undefined if no draft stored. + */ + async resumeDraft(contextLevel: string, contextInstanceId: number, elementId: string, extraParams: {[name: string]: any}, + pageInstance: string, siteId?: string): Promise { + + 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 this.sitesProvider.getSiteDb(siteId); + + entry.pageinstance = pageInstance; + entry.timemodified = Date.now(); + + await db.insertRecord(this.DRAFT_TABLE, entry); + } catch (error) { + // Ignore errors saving the draft. It shouldn't happen. + } + + return entry.drafttext; + } catch (error) { + // No draft stored. Store an empty draft to save the pageinstance. + await this.saveDraft(contextLevel, contextInstanceId, elementId, extraParams, pageInstance, '', 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 siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async saveDraft(contextLevel: string, contextInstanceId: number, elementId: string, extraParams: {[name: string]: any}, + pageInstance: string, draftText: string, siteId?: string): Promise { + + let timecreated = Date.now(); + let entry: CoreEditorDraft; + + // Check if there is a draft already stored. + try { + entry = await this.getDraft(contextLevel, contextInstanceId, elementId, extraParams, siteId); + + timecreated = entry.timecreated; + } catch (error) { + // No draft already stored. + } + + if (entry && entry.pageinstance != pageInstance) { + this.logger.warning(`Discarding draft because of pageinstance. Context '${contextLevel}' '${contextInstanceId}', ` + + `element '${elementId}'`); + throw null; + } + + const db = await this.sitesProvider.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(); + + await db.insertRecord(this.DRAFT_TABLE, data); + } +} + +/** + * Primary data to identify a stored draft. + */ +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. + */ +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. +};