@print-one/grapesjs
Version:
Free and Open Source Web Builder Framework
486 lines (439 loc) • 12.9 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/asset_manager/config/config.ts)
* ```js
* const editor = grapesjs.init({
* assetManager: {
* // options
* }
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
*
* ```js
* const assetManager = editor.AssetManager;
* ```
*
* ## Available Events
* * `asset:open` - Asset Manager opened.
* * `asset:close` - Asset Manager closed.
* * `asset:add` - Asset added. The [Asset] is passed as an argument to the callback.
* * `asset:remove` - Asset removed. The [Asset] is passed as an argument to the callback.
* * `asset:update` - Asset updated. The updated [Asset] and the object containing changes are passed as arguments to the callback.
* * `asset:upload:start` - Before the upload is started.
* * `asset:upload:end` - After the upload is ended.
* * `asset:upload:error` - On any error in upload, passes the error as an argument.
* * `asset:upload:response` - On upload response, passes the result as an argument.
* * `asset` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
* * `asset:custom` - Event for handling custom Asset Manager UI.
*
* ## Methods
* * [open](#open)
* * [close](#close)
* * [isOpen](#isopen)
* * [add](#add)
* * [get](#get)
* * [getAll](#getall)
* * [getAllVisible](#getallvisible)
* * [remove](#remove)
* * [getContainer](#getcontainer)
*
* [Asset]: asset.html
*
* @module Assets
*/
import { debounce, isFunction } from 'underscore';
import { ItemManagerModule } from '../abstract/Module';
import EditorModel from '../editor/model/Editor';
import defaults, { AssetManagerConfig } from './config/config';
import Asset from './model/Asset';
import Assets from './model/Assets';
import AssetsView from './view/AssetsView';
import FileUploaderView from './view/FileUploader';
export type AssetEvent =
| 'asset'
| 'asset:open'
| 'asset:close'
| 'asset:add'
| 'asset:remove'
| 'asset:update'
| 'asset:custom'
| 'asset:upload:start'
| 'asset:upload:end'
| 'asset:upload:error'
| 'asset:upload:response';
export const evAll = 'asset';
export const evPfx = `${evAll}:`;
export const evSelect = `${evPfx}select`;
export const evUpdate = `${evPfx}update`;
export const evAdd = `${evPfx}add`;
export const evRemove = `${evPfx}remove`;
export const evRemoveBefore = `${evRemove}:before`;
export const evCustom = `${evPfx}custom`;
export const evOpen = `${evPfx}open`;
export const evClose = `${evPfx}close`;
export const evUpload = `${evPfx}upload`;
export const evUploadStart = `${evUpload}:start`;
export const evUploadEnd = `${evUpload}:end`;
export const evUploadError = `${evUpload}:error`;
export const evUploadRes = `${evUpload}:response`;
const assetCmd = 'open-assets';
const assetEvents = {
all: evAll,
select: evSelect,
update: evUpdate,
add: evAdd,
remove: evRemove,
removeBefore: evRemoveBefore,
custom: evCustom,
open: evOpen,
close: evClose,
uploadStart: evUploadStart,
uploadEnd: evUploadEnd,
uploadError: evUploadError,
uploadResponse: evUploadRes,
};
// TODO
type AssetProps = Record<string, any>;
type OpenOptions = {
select?: (asset: Asset, complete: boolean) => void;
types?: string[];
accept?: string;
target?: any;
};
export default class AssetManager extends ItemManagerModule<AssetManagerConfig, Assets> {
storageKey = 'assets';
Asset = Asset;
Assets = Assets;
assetsVis: Assets;
am?: AssetsView;
fu?: FileUploaderView;
_bhv?: any;
events!: typeof assetEvents;
/**
* Initialize module
* @param {Object} config Configurations
* @private
*/
constructor(em: EditorModel) {
// @ts-ignore
super(em, 'AssetManager', new Assets([], em), assetEvents, defaults);
const { all, config } = this;
// @ts-ignore
this.assetsVis = new Assets([]);
// @ts-ignore
const ppfx = config.pStylePrefix;
if (ppfx) {
config.stylePrefix = `${ppfx}${config.stylePrefix}`;
}
// Setup the sync between the global and public collections
all.on('add', (model: Asset) => this.getAllVisible().add(model));
all.on('remove', (model: Asset) => this.getAllVisible().remove(model));
this.__onAllEvent = debounce(() => this.__trgCustom(), 0);
return this;
}
__propEv(ev: string, ...data: any[]) {
this.em.trigger(ev, ...data);
this.getAll().trigger(ev, ...data);
}
__trgCustom() {
const bhv = this.__getBehaviour();
const custom = this.getConfig().custom;
if (!bhv.container && !(custom as any).open) {
return;
}
this.em.trigger(this.events.custom, this.__customData());
}
__customData() {
const bhv = this.__getBehaviour();
return {
am: this as AssetManager,
open: this.isOpen(),
assets: this.getAll().models,
types: bhv.types || [],
container: bhv.container,
close: () => this.close(),
remove: (asset: string | Asset, opts?: Record<string, any>) => this.remove(asset, opts),
select: (asset: Asset, complete: boolean) => {
const res = this.add(asset);
isFunction(bhv.select) && bhv.select(res, complete);
},
// extra
options: bhv.options || {},
};
}
/**
* Open the asset manager.
* @param {Object} [options] Options for the asset manager.
* @param {Array<String>} [options.types=['image']] Types of assets to show.
* @param {Function} [options.select] Type of operation to perform on asset selection. If not specified, nothing will happen.
* @example
* assetManager.open({
* select(asset, complete) {
* const selected = editor.getSelected();
* if (selected && selected.is('image')) {
* selected.addAttributes({ src: asset.getSrc() });
* // The default AssetManager UI will trigger `select(asset, false)` on asset click
* // and `select(asset, true)` on double-click
* complete && assetManager.close();
* }
* }
* });
* // with your custom types (you should have assets with those types declared)
* assetManager.open({ types: ['doc'], ... });
*/
open(options: OpenOptions = {}) {
const cmd = this.em.Commands;
cmd.run(assetCmd, {
types: ['image'],
select: () => {},
...options,
});
}
/**
* Close the asset manager.
* @example
* assetManager.close();
*/
close() {
const cmd = this.em.Commands;
cmd.stop(assetCmd);
}
/**
* Checks if the asset manager is open
* @returns {Boolean}
* @example
* assetManager.isOpen(); // true | false
*/
isOpen() {
const cmd = this.em.Commands;
return !!cmd?.isActive(assetCmd);
}
/**
* Add new asset/s to the collection. URLs are supposed to be unique
* @param {String|Object|Array<String>|Array<Object>} asset URL strings or an objects representing the resource.
* @param {Object} [opts] Options
* @returns {[Asset]}
* @example
* // As strings
* assetManager.add('http://img.jpg');
* assetManager.add(['http://img.jpg', './path/to/img.png']);
*
* // Using objects you can indicate the type and other meta informations
* assetManager.add({
* // type: 'image', // image is default
* src: 'http://img.jpg',
* height: 300,
* width: 200,
* });
* assetManager.add([{ src: 'img2.jpg' }, { src: 'img2.png' }]);
*/
add(asset: string | AssetProps | (string | AssetProps)[], opts: Record<string, any> = {}) {
// Put the model at the beginning
if (typeof opts.at == 'undefined') {
opts.at = 0;
}
return this.all.add(asset, opts);
}
/**
* Return asset by URL
* @param {String} src URL of the asset
* @returns {[Asset]|null}
* @example
* const asset = assetManager.get('http://img.jpg');
*/
get(src: string): Asset | null {
return this.all.where({ src })[0] || null;
}
/**
* Return the global collection, containing all the assets
* @returns {Collection<[Asset]>}
*/
getAll() {
return this.all;
}
/**
* Return the visible collection, which contains assets actually rendered
* @returns {Collection<[Asset]>}
*/
getAllVisible() {
return this.assetsVis;
}
/**
* Remove asset
* @param {String|[Asset]} asset Asset or asset URL
* @returns {[Asset]} Removed asset
* @example
* const removed = assetManager.remove('http://img.jpg');
* // or by passing the Asset
* const asset = assetManager.get('http://img.jpg');
* assetManager.remove(asset);
*/
remove(asset: string | Asset, opts?: Record<string, any>) {
return this.__remove(asset, opts);
}
store() {
return this.getProjectData();
}
load(data: Record<string, any>) {
return this.loadProjectData(data);
}
/**
* Return the Asset Manager Container
* @returns {HTMLElement}
*/
getContainer() {
const bhv = this.__getBehaviour();
return bhv.container || this.am?.el;
}
/**
* Get assets element container
* @returns {HTMLElement}
* @private
*/
getAssetsEl() {
return this.am?.el.querySelector('[data-el=assets]');
}
/**
* Render assets
* @param {array} assets Assets to render, without the argument will render all global assets
* @returns {HTMLElement}
* @private
* @example
* // Render all assets
* assetManager.render();
*
* // Render some of the assets
* const assets = assetManager.getAll();
* assetManager.render(assets.filter(
* asset => asset.get('category') == 'cats'
* ));
*/
render(assts?: Asset[]) {
if (this.getConfig().custom) return;
const toRender = assts || this.getAll().models;
if (!this.am) {
const obj = this.__viewParams();
obj.fu = this.FileUploader();
this.am = new AssetsView({ ...obj });
this.am.render();
}
this.assetsVis.reset(toRender);
return this.getContainer();
}
__viewParams() {
return {
collection: this.assetsVis, // Collection visible in asset manager
globalCollection: this.all,
config: this.config,
module: this as AssetManager,
fu: undefined as any,
};
}
/**
* Add new type. If you want to get more about type definition we suggest to read the [module's page](/modules/Assets.html)
* @param {string} id Type ID
* @param {Object} definition Definition of the type. Each definition contains
* `model` (business logic), `view` (presentation logic)
* and `isType` function which recognize the type of the
* passed entity
* @private
* @example
* assetManager.addType('my-type', {
* model: {},
* view: {},
* isType: (value) => {},
* })
*/
addType(id: string, definition: any) {
this.getAll().addType(id, definition);
}
/**
* Get type
* @param {string} id Type ID
* @returns {Object} Type definition
* @private
*/
getType(id: string) {
return this.getAll().getType(id);
}
/**
* Get types
* @returns {Array}
* @private
*/
getTypes() {
return this.getAll().getTypes();
}
//-------
AssetsView() {
return this.am;
}
FileUploader() {
if (!this.fu) {
this.fu = new FileUploaderView(this.__viewParams());
}
return this.fu;
}
onLoad() {
this.getAll().reset(this.config.assets);
const { em, events } = this;
em.on(`run:${assetCmd}`, () => this.__propEv(events.open));
em.on(`stop:${assetCmd}`, () => this.__propEv(events.close));
}
postRender(editorView: any) {
this.config.dropzone && this.fu?.initDropzone(editorView);
}
/**
* Set new target
* @param {Object} m Model
* @private
* */
setTarget(m: any) {
this.assetsVis.target = m;
}
/**
* Set callback after asset was selected
* @param {Object} f Callback function
* @private
* */
onSelect(f: any) {
this.assetsVis.onSelect = f;
}
/**
* Set callback to fire when the asset is clicked
* @param {function} func
* @private
*/
onClick(func: any) {
// @ts-ignore
this.config.onClick = func;
}
/**
* Set callback to fire when the asset is double clicked
* @param {function} func
* @private
*/
onDblClick(func: any) {
// @ts-ignore
this.config.onDblClick = func;
}
__behaviour(opts = {}) {
return (this._bhv = {
...(this._bhv || {}),
...opts,
});
}
__getBehaviour(opts = {}) {
return this._bhv || {};
}
destroy() {
this.all.stopListening();
this.all.reset();
this.assetsVis.stopListening();
this.assetsVis.reset();
this.fu?.remove();
this.am?.remove();
this._bhv = {};
}
}