@print-one/grapesjs
Version:
Free and Open Source Web Builder Framework
409 lines (363 loc) • 10.8 kB
text/typescript
/**
* You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/GrapesJS/grapesjs/blob/master/src/storage_manager/config/config.ts)
* ```js
* const editor = grapesjs.init({
* storageManager: {
* // options
* }
* })
* ```
*
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('storage:start', () => { ... });
*
* // Use the API
* const storageManager = editor.Storage;
* storageManager.add(...);
* ```
*
* ## Available Events
* * `storage:start` - Before the storage request is started
* * `storage:start:store` - Before the store request. The object to store is passed as an argument (which you can edit)
* * `storage:start:load` - Before the load request. Items to load are passed as an argument (which you can edit)
* * `storage:load` - Triggered when something was loaded from the storage, loaded object passed as an argument
* * `storage:store` - Triggered when something is stored to the storage, stored object passed as an argument
* * `storage:end` - After the storage request is ended
* * `storage:end:store` - After the store request
* * `storage:end:load` - After the load request
* * `storage:error` - On any error on storage request, passes the error as an argument
* * `storage:error:store` - Error on store request, passes the error as an argument
* * `storage:error:load` - Error on load request, passes the error as an argument
*
* ## Methods
* * [getConfig](#getconfig)
* * [isAutosave](#isautosave)
* * [setAutosave](#setautosave)
* * [getStepsBeforeSave](#getstepsbeforesave)
* * [setStepsBeforeSave](#setstepsbeforesave)
* * [getStorages](#getstorages)
* * [getCurrent](#getcurrent)
* * [getCurrentStorage](#getcurrentstorage)
* * [setCurrent](#setcurrent)
* * [getStorageOptions](#getstorageoptions)
* * [add](#add)
* * [get](#get)
* * [store](#store)
* * [load](#load)
*
* @module Storage
*/
import { isEmpty, isFunction } from 'underscore';
import { Module } from '../abstract';
import defaults, { StorageManagerConfig } from './config/config';
import LocalStorage from './model/LocalStorage';
import RemoteStorage from './model/RemoteStorage';
import EditorModel from '../editor/model/Editor';
import IStorage, { StorageOptions, ProjectData } from './model/IStorage';
export type StorageEvent =
| 'storage:start'
| 'storage:start:store'
| 'storage:start:load'
| 'storage:load'
| 'storage:store'
| 'storage:end'
| 'storage:end:store'
| 'storage:end:load'
| 'storage:error'
| 'storage:error:store'
| 'storage:error:load';
const eventStart = 'storage:start';
const eventAfter = 'storage:after';
const eventEnd = 'storage:end';
const eventError = 'storage:error';
const STORAGE_LOCAL = 'local';
const STORAGE_REMOTE = 'remote';
export default class StorageManager extends Module<
StorageManagerConfig & { name?: string; _disable?: boolean; currentStorage?: string }
> {
storages: Record<string, IStorage> = {};
constructor(em: EditorModel) {
super(em, 'StorageManager', defaults);
const { config } = this;
if (config._disable) config.type = undefined;
this.storages = {};
this.add(STORAGE_LOCAL, new LocalStorage());
this.add(STORAGE_REMOTE, new RemoteStorage());
this.setCurrent(config.type!);
}
/**
* Get configuration object
* @name getConfig
* @function
* @return {Object}
*/
/**
* Check if autosave is enabled.
* @returns {Boolean}
* */
isAutosave() {
return !!this.config.autosave;
}
/**
* Set autosave value.
* @param {Boolean} value
* */
setAutosave(value: boolean) {
this.config.autosave = !!value;
return this;
}
/**
* Returns number of steps required before trigger autosave.
* @returns {Number}
* */
getStepsBeforeSave() {
return this.config.stepsBeforeSave!;
}
/**
* Set steps required before trigger autosave.
* @param {Number} value
* */
setStepsBeforeSave(value: number) {
this.config.stepsBeforeSave = value;
return this;
}
/**
* Add new storage.
* @param {String} type Storage type
* @param {Object} storage Storage definition
* @param {Function} storage.load Load method
* @param {Function} storage.store Store method
* @example
* storageManager.add('local2', {
* async load(storageOptions) {
* // ...
* },
* async store(data, storageOptions) {
* // ...
* },
* });
* */
add<T extends StorageOptions>(type: string, storage: IStorage<T>) {
// @ts-ignore
this.storages[type] = storage;
return this;
}
/**
* Return storage by type.
* @param {String} type Storage type
* @returns {Object|null}
* */
get<T extends StorageOptions>(type: string): IStorage<T> | undefined {
return this.storages[type];
}
/**
* Get all storages.
* @returns {Object}
* */
getStorages() {
return this.storages;
}
/**
* Get current storage type.
* @returns {String}
* */
getCurrent() {
return this.config.currentStorage!;
}
/**
* Set current storage type.
* @param {String} type Storage type
* */
setCurrent(type: string) {
this.getConfig().currentStorage = type;
return this;
}
getCurrentStorage() {
return this.get(this.getCurrent());
}
/**
* Get storage options by type.
* @param {String} type Storage type
* @returns {Object}
* */
getStorageOptions(type: string) {
return this.getCurrentOptions(type);
}
/**
* Store data in the current storage.
* @param {Object} data Project data.
* @param {Object} [options] Storage options.
* @returns {Object} Stored data.
* @example
* const data = editor.getProjectData();
* await storageManager.store(data);
* */
async store(data: ProjectData, options: StorageOptions = {}) {
const st = this.getCurrentStorage();
const opts = { ...this.getCurrentOptions(), ...options };
const recovery = this.getRecoveryStorage();
const recoveryOpts = this.getCurrentOptions(STORAGE_LOCAL);
try {
await this.__exec(st!, opts, data);
recovery && (await this.__exec(recovery, recoveryOpts, {}));
} catch (error) {
if (recovery) {
await this.__exec(recovery, recoveryOpts, data);
} else {
throw error;
}
}
return data;
}
/**
* Load resource from the current storage by keys
* @param {Object} [options] Storage options.
* @returns {Object} Loaded data.
* @example
* const data = await storageManager.load();
* editor.loadProjectData(data);
* */
async load(options = {}) {
const st = this.getCurrentStorage();
const opts = { ...this.getCurrentOptions(), ...options };
const recoveryStorage = this.getRecoveryStorage();
let result: ProjectData | undefined;
if (recoveryStorage) {
const recoveryData = await this.__exec(recoveryStorage, this.getCurrentOptions(STORAGE_LOCAL));
if (!isEmpty(recoveryData)) {
try {
await this.__askRecovery();
result = recoveryData;
} catch (error) {}
}
}
if (!result) {
result = await this.__exec(st!, opts);
}
return result || {};
}
__askRecovery() {
const { em } = this;
const recovery = this.getRecovery();
return new Promise((res, rej) => {
if (isFunction(recovery)) {
recovery(res, rej, em?.getEditor());
} else {
confirm(em?.t('storageManager.recover')) ? res(null) : rej();
}
});
}
getRecovery(): StorageManagerConfig['recovery'] {
return this.config.recovery;
}
getRecoveryStorage() {
const recovery = this.getRecovery();
return recovery && this.getCurrent() === STORAGE_REMOTE && this.get(STORAGE_LOCAL);
}
async __exec(storage: IStorage, opts: StorageOptions, data?: ProjectData) {
const ev = data ? 'store' : 'load';
const { onStore, onLoad } = this.getConfig();
let result;
this.onStart(ev, data);
if (!storage) {
return data || {};
}
try {
const editor = this.em?.getEditor();
let response: any;
if (data) {
let toStore = (onStore && (await onStore(data, editor))) || data;
toStore = (opts.onStore && (await opts.onStore(toStore, editor))) || toStore;
response = await storage.store(toStore, opts);
result = data;
} else {
response = await storage.load(opts);
result = this.__clearKeys(response);
result = (opts.onLoad && (await opts.onLoad(result, editor))) || result;
result = (onLoad && (await onLoad(result, editor))) || result;
}
this.onAfter(ev, result, response);
this.onEnd(ev, result);
} catch (error) {
this.onError(ev, error);
throw error;
}
return result;
}
__clearKeys(data: ProjectData = {}) {
const config = this.getConfig();
const reg = new RegExp(`^${config.id}`);
const result: ProjectData = {};
for (let itemKey in data) {
const itemKeyR = itemKey.replace(reg, '');
result[itemKeyR] = data[itemKey];
}
return result;
}
getCurrentOptions(type?: string): StorageOptions {
const config = this.getConfig();
const current = type || this.getCurrent();
return config.options![current] || {};
}
/**
* On start callback
* @private
*/
onStart(ctx: string, data?: ProjectData) {
const { em } = this;
if (em) {
em.trigger(eventStart);
ctx && em.trigger(`${eventStart}:${ctx}`, data);
}
}
/**
* On after callback (before passing data to the callback)
* @private
*/
onAfter(ctx: string, data: ProjectData, response: any) {
const { em } = this;
if (em) {
em.trigger(eventAfter);
em.trigger(`${eventAfter}:${ctx}`, data, response);
em.trigger(`storage:${ctx}`, data, response);
}
}
/**
* On end callback
* @private
*/
onEnd(ctx: string, data: ProjectData) {
const { em } = this;
if (em) {
em.trigger(eventEnd);
ctx && em.trigger(`${eventEnd}:${ctx}`, data);
}
}
/**
* On error callback
* @private
*/
onError(ctx: string, data: any) {
const { em } = this;
if (em) {
em.trigger(eventError, data);
ctx && em.trigger(`${eventError}:${ctx}`, data);
this.onEnd(ctx, data);
}
}
/**
* Check if autoload is possible
* @return {Boolean}
* @private
* */
canAutoload() {
const storage = this.getCurrentStorage();
return !!storage && !!this.config.autoload;
}
destroy() {
this.storages = {};
}
}