Browse Source

Migrate Asset Manager to TS (#4604)

* Update assets config

* Update index TS

* Up TS

* Init TS update for assets index

* Move assets models to TS

* Update AssetView

* Update AssetImageView

* Update AssetsView

* Update FileUploaderView

* Update FileUploader

* Fix asset tests

* Use data-input attribute for events in FileUploader

* Up

* Update JSDoc config link

* Up docs
pull/4615/head
Artur Arseniev 4 years ago
committed by GitHub
parent
commit
10ec5890cb
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      docs/api.js
  2. 6
      docs/api/assets.md
  3. 136
      index.d.ts
  4. 6
      src/abstract/Module.ts
  5. 110
      src/asset_manager/config/config.js
  6. 156
      src/asset_manager/config/config.ts
  7. 472
      src/asset_manager/index.js
  8. 463
      src/asset_manager/index.ts
  9. 8
      src/asset_manager/model/Asset.ts
  10. 0
      src/asset_manager/model/AssetImage.ts
  11. 7
      src/asset_manager/model/Assets.ts
  12. 11
      src/asset_manager/view/AssetImageView.ts
  13. 40
      src/asset_manager/view/AssetView.ts
  14. 49
      src/asset_manager/view/AssetsView.ts
  15. 290
      src/asset_manager/view/FileUploader.ts
  16. 3
      test/specs/asset_manager/index.js

4
docs/api.js

@ -12,8 +12,8 @@ async function generateDocs () {
await Promise.all([
['editor/index.ts', 'editor.md'],
['asset_manager/index.js', 'assets.md'],
['asset_manager/model/Asset.js', 'asset.md'],
['asset_manager/index.ts', 'assets.md'],
['asset_manager/model/Asset.ts', 'asset.md'],
['block_manager/index.js', 'block_manager.md'],
['block_manager/model/Block.js', 'block.md'],
['commands/index.js', 'commands.md'],

6
docs/api/assets.md

@ -1,6 +1,6 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## AssetManager
## Assets
You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object][1]
@ -160,7 +160,7 @@ Remove asset
### Parameters
* `asset` **([String][13] | [Asset])** Asset or asset URL
* `opts`
* `opts` **Record<[string][13], any>?**
### Examples
@ -179,7 +179,7 @@ Return the Asset Manager Container
Returns **[HTMLElement][16]**
[1]: https://github.com/artf/grapesjs/blob/master/src/asset_manager/config/config.js
[1]: https://github.com/artf/grapesjs/blob/master/src/asset_manager/config/config.ts
[2]: #open

136
index.d.ts

@ -258,31 +258,135 @@ declare namespace grapesjs {
}
interface AssetManagerConfig {
assets?: Array<object>;
/**
* Default assets.
* @example
* [
* 'https://...image1.png',
* 'https://...image2.png',
* {type: 'image', src: 'https://...image3.png', someOtherCustomProp: 1}
* ]
*/
assets?: (string | Record<string, any>)[];
/**
* Content to add where there is no assets to show.
* @default ''
* @example 'No <b>assets</b> here, drag to upload'
*/
noAssets?: string;
/**
* Style prefix
* @default 'am-'
*/
stylePrefix?: string;
upload?: boolean;
/**
* Upload endpoint, set `false` to disable upload.
* @example 'https://endpoint/upload/assets'
*/
upload?: false | string;
/**
* The name used in POST to pass uploaded files.
* @default 'files'
*/
uploadName?: string;
headers?: object;
params?: object;
/**
* Custom headers to pass with the upload request.
* @default {}
*/
headers?: Record<string, any>;
/**
* Custom parameters to pass with the upload request, eg. csrf token.
* @default {}
*/
params?: Record<string, any>;
/**
* The credentials setting for the upload request, eg. 'include', 'omit'.
* @default 'include'
*/
credentials?: RequestCredentials;
/**
* Allow uploading multiple files per request. If disabled filename will not have '[]' appended.
* @default true
*/
multiUpload?: boolean;
/**
* If true, tries to add automatically uploaded assets. To make it work the server should respond with a JSON containing assets in a data key, eg:
* { data: [ 'https://.../image.png', {src: 'https://.../image2.png'} ]
* @default true
*/
autoAdd?: boolean;
uploadText?: string;
addBtnText?: string;
customFetch?: Function;
uploadFile?: Function;
/**
* To upload your assets, the module uses Fetch API. With this option you can overwrite it with your own logic. The custom function should return a Promise.
* @example
* customFetch: (url, options) => axios(url, { data: options.body }),
*/
customFetch?: (url: string, options: Record<string, any>) => Promise<void>;
/**
* Custom uploadFile function.
* Differently from the `customFetch` option, this gives a total control over the uploading process, but you also have to emit all `asset:upload:*` events b
* y yourself (if you need to use them somewhere).
* @example
* uploadFile: (ev) => {
* const files = ev.dataTransfer ? ev.dataTransfer.files : ev.target.files;
* // ...send somewhere
* }
*/
uploadFile?: (ev: DragEvent) => void;
/**
* In the absence of 'uploadFile' or 'upload' assets will be embedded as Base64.
* @default true
*/
embedAsBase64?: boolean;
handleAdd?: Function;
/**
* Handle the image url submit from the built-in 'Add image' form.
* @example
* handleAdd: (textFromInput) => {
* // some check...
* editor.AssetManager.add(textFromInput);
* }
*/
handleAdd?: (value: string) => void;
/**
* Method called before upload, on return false upload is canceled.
* @example
* beforeUpload: (files) => {
* // logic...
* const stopUpload = true;
* if(stopUpload) return false;
* }
*/
beforeUpload?: (files: any) => void | false;
/**
* Toggles visiblity of assets url input
* @default true
*/
showUrlInput?: boolean;
/**
* Avoid rendering the default asset manager.
* @default false
*/
custom?:
| boolean
| {
open?: (props: any) => void;
close?: (props: any) => void;
};
/**
* Enable an upload dropzone on the entire editor (not document) when dragging files over it.
* If active the dropzone disable/hide the upload dropzone in asset modal, otherwise you will get double drops (#507).
* @deprecated
*/
dropzone?: boolean;
openAssetsOnDrop?: number;
/**
* Open the asset manager once files are been dropped via the dropzone.
* @deprecated
*/
openAssetsOnDrop?: boolean;
/**
* Any dropzone content to append inside dropzone element
* @deprecated
*/
dropzoneContent?: string;
modalTitle?: string;
inputPlaceholder?: string;
custom?: boolean | {
open?: (props: any) => void,
close?: (props: any) => void,
};
}
interface CanvasConfig {

6
src/abstract/Module.ts

@ -1,4 +1,4 @@
import { isElement, isUndefined } from 'underscore';
import { isElement, isUndefined, isString } from 'underscore';
import { Collection, View } from '../common';
import EditorModel from '../editor/model/Editor';
import { createId, isDef, deepMerge } from '../utils/mixins';
@ -108,8 +108,8 @@ export abstract class ItemManagerModule<
protected all: TCollection;
view?: View;
constructor(em: EditorModel, moduleName: string, all: any, events?: any) {
super(em, moduleName);
constructor(em: EditorModel, moduleName: string, all: any, events?: any, defaults?: TConf) {
super(em, moduleName, defaults);
this.all = all;
this.events = events;
this.__initListen();

110
src/asset_manager/config/config.js

@ -1,110 +0,0 @@
export default {
// Default assets
// eg. [
// 'https://...image1.png',
// 'https://...image2.png',
// {type: 'image', src: 'https://...image3.png', someOtherCustomProp: 1},
// ..
// ]
assets: [],
// Content to add where there is no assets to show
// eg. 'No <b>assets</b> here, drag to upload'
noAssets: '',
// Style prefix
stylePrefix: 'am-',
// Upload endpoint, set `false` to disable upload
// upload: 'https://endpoint/upload/assets',
// upload: false,
upload: 0,
// The name used in POST to pass uploaded files
uploadName: 'files',
// Custom headers to pass with the upload request
headers: {},
// Custom parameters to pass with the upload request, eg. csrf token
params: {},
// The credentials setting for the upload request, eg. 'include', 'omit'
credentials: 'include',
// Allow uploading multiple files per request.
// If disabled filename will not have '[]' appended
multiUpload: true,
// If true, tries to add automatically uploaded assets.
// To make it work the server should respond with a JSON containing assets
// in a data key, eg:
// {
// data: [
// 'https://.../image.png',
// ...
// {src: 'https://.../image2.png'},
// ...
// ]
// }
autoAdd: true,
// To upload your assets, the module uses Fetch API, with this option you
// overwrite it with something else.
// It should return a Promise
// @example
// customFetch: (url, options) => axios(url, { data: options.body }),
customFetch: null,
// Custom uploadFile function.
// Differently from the `customFetch` option, this gives a total control
// over the uploading process, but you also have to emit all `asset:upload:*` events
// by yourself (if you need to use them somewhere)
// @example
// uploadFile: (e) => {
// var files = e.dataTransfer ? e.dataTransfer.files : e.target.files;
// // ...send somewhere
// }
uploadFile: null,
// In the absence of 'uploadFile' or 'upload' assets will be embedded as Base64
embedAsBase64: true,
// Handle the image url submit from the built-in 'Add image' form
// @example
// handleAdd: (textFromInput) => {
// // some check...
// editor.AssetManager.add(textFromInput);
// }
handleAdd: null,
// Method called before upload, on return false upload is canceled.
// @example
// beforeUpload: (files) => {
// // logic...
// var stopUpload = true;
// if(stopUpload) return false;
// }
beforeUpload: null,
// Toggles visiblity of assets url input
showUrlInput: true,
// Avoid rendering the default asset manager.
custom: false,
// WARNING: all the options below are considered DEPRECATED.
// ---------------------------------------------------------------
// Enable an upload dropzone on the entire editor (not document) when dragging
// files over it
// If active the dropzone disable/hide the upload dropzone in asset modal,
// otherwise you will get double drops (#507)
dropzone: false,
// Open the asset manager once files are been dropped via the dropzone
openAssetsOnDrop: 1,
// Any dropzone content to append inside dropzone element
dropzoneContent: '',
};

156
src/asset_manager/config/config.ts

@ -0,0 +1,156 @@
export interface AssetManagerConfig {
/**
* Default assets.
* @example
* [
* 'https://...image1.png',
* 'https://...image2.png',
* {type: 'image', src: 'https://...image3.png', someOtherCustomProp: 1}
* ]
*/
assets?: (string | Record<string, any>)[];
/**
* Content to add where there is no assets to show.
* @default ''
* @example 'No <b>assets</b> here, drag to upload'
*/
noAssets?: string;
/**
* Style prefix
* @default 'am-'
*/
stylePrefix?: string;
/**
* Upload endpoint, set `false` to disable upload.
* @example 'https://endpoint/upload/assets'
*/
upload?: false | string;
/**
* The name used in POST to pass uploaded files.
* @default 'files'
*/
uploadName?: string;
/**
* Custom headers to pass with the upload request.
* @default {}
*/
headers?: Record<string, any>;
/**
* Custom parameters to pass with the upload request, eg. csrf token.
* @default {}
*/
params?: Record<string, any>;
/**
* The credentials setting for the upload request, eg. 'include', 'omit'.
* @default 'include'
*/
credentials?: RequestCredentials;
/**
* Allow uploading multiple files per request. If disabled filename will not have '[]' appended.
* @default true
*/
multiUpload?: boolean;
/**
* If true, tries to add automatically uploaded assets. To make it work the server should respond with a JSON containing assets in a data key, eg:
* { data: [ 'https://.../image.png', {src: 'https://.../image2.png'} ]
* @default true
*/
autoAdd?: boolean;
/**
* To upload your assets, the module uses Fetch API. With this option you can overwrite it with your own logic. The custom function should return a Promise.
* @example
* customFetch: (url, options) => axios(url, { data: options.body }),
*/
customFetch?: (url: string, options: Record<string, any>) => Promise<void>;
/**
* Custom uploadFile function.
* Differently from the `customFetch` option, this gives a total control over the uploading process, but you also have to emit all `asset:upload:*` events b
* y yourself (if you need to use them somewhere).
* @example
* uploadFile: (ev) => {
* const files = ev.dataTransfer ? ev.dataTransfer.files : ev.target.files;
* // ...send somewhere
* }
*/
uploadFile?: (ev: DragEvent) => void;
/**
* In the absence of 'uploadFile' or 'upload' assets will be embedded as Base64.
* @default true
*/
embedAsBase64?: boolean;
/**
* Handle the image url submit from the built-in 'Add image' form.
* @example
* handleAdd: (textFromInput) => {
* // some check...
* editor.AssetManager.add(textFromInput);
* }
*/
handleAdd?: (value: string) => void;
/**
* Method called before upload, on return false upload is canceled.
* @example
* beforeUpload: (files) => {
* // logic...
* const stopUpload = true;
* if(stopUpload) return false;
* }
*/
beforeUpload?: (files: any) => void | false;
/**
* Toggles visiblity of assets url input
* @default true
*/
showUrlInput?: boolean;
/**
* Avoid rendering the default asset manager.
* @default false
*/
custom?:
| boolean
| {
open?: (props: any) => void;
close?: (props: any) => void;
};
/**
* Enable an upload dropzone on the entire editor (not document) when dragging files over it.
* If active the dropzone disable/hide the upload dropzone in asset modal, otherwise you will get double drops (#507).
* @deprecated
*/
dropzone?: boolean;
/**
* Open the asset manager once files are been dropped via the dropzone.
* @deprecated
*/
openAssetsOnDrop?: boolean;
/**
* Any dropzone content to append inside dropzone element
* @deprecated
*/
dropzoneContent?: string;
}
const config: AssetManagerConfig = {
assets: [],
noAssets: '',
stylePrefix: 'am-',
upload: '',
uploadName: 'files',
headers: {},
params: {},
credentials: 'include',
multiUpload: true,
autoAdd: true,
customFetch: undefined,
uploadFile: undefined,
embedAsBase64: true,
handleAdd: undefined,
beforeUpload: undefined,
showUrlInput: true,
custom: false,
dropzone: false,
openAssetsOnDrop: true,
dropzoneContent: '',
};
export default config;

472
src/asset_manager/index.js

@ -1,472 +0,0 @@
/**
* You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/asset_manager/config/config.js)
* ```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 AssetManager
*/
import { debounce, isFunction } from 'underscore';
import { Module } from '../common';
import defaults from './config/config';
import Asset from './model/Assets';
import Assets from './model/Assets';
import AssetsView from './view/AssetsView';
import FileUpload from './view/FileUploader';
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`;
export default () => {
let c = {};
let assets, assetsVis, am, fu;
const assetCmd = 'open-assets';
return {
...Module,
name: 'AssetManager',
storageKey: 'assets',
Asset,
Assets,
events: {
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,
},
init(config = {}) {
c = { ...defaults, ...config };
const ppfx = c.pStylePrefix;
const { em } = c;
this.config = c;
this.em = em;
if (ppfx) {
c.stylePrefix = ppfx + c.stylePrefix;
}
// Global assets collection
assets = new Assets([]);
assetsVis = new Assets([]);
this.all = assets;
this.__initListen();
// Setup the sync between the global and public collections
assets.on('add', model => this.getAllVisible().add(model));
assets.on('remove', model => this.getAllVisible().remove(model));
return this;
},
__propEv(ev, ...data) {
this.em.trigger(ev, ...data);
this.getAll().trigger(ev, ...data);
},
__onAllEvent: debounce(function () {
this.__trgCustom();
}),
__trgCustom() {
const bhv = this.__getBehaviour();
if (!bhv.container && !this.getConfig().custom.open) {
return;
}
this.em.trigger(this.events.custom, this.__customData());
},
__customData() {
const bhv = this.__getBehaviour();
return {
am: this,
open: this.isOpen(),
assets: this.getAll().models,
types: bhv.types || [],
container: bhv.container,
close: () => this.close(),
remove: (...args) => this.remove(...args),
select: (asset, complete) => {
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 = {}) {
const cmd = this.em.get('Commands');
cmd.run(assetCmd, {
types: ['image'],
select: () => {},
...options,
});
},
/**
* Close the asset manager.
* @example
* assetManager.close();
*/
close() {
const cmd = this.em.get('Commands');
cmd.stop(assetCmd);
},
/**
* Checks if the asset manager is open
* @returns {Boolean}
* @example
* assetManager.isOpen(); // true | false
*/
isOpen() {
const cmd = this.em.get('Commands');
return !!(cmd && 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, opts = {}) {
// Put the model at the beginning
if (typeof opts.at == 'undefined') {
opts.at = 0;
}
return assets.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) {
return assets.where({ src })[0] || null;
},
/**
* Return the global collection, containing all the assets
* @returns {Collection<[Asset]>}
*/
getAll() {
return assets;
},
/**
* Return the visible collection, which contains assets actually rendered
* @returns {Collection<[Asset]>}
*/
getAllVisible() {
return 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, opts) {
return this.__remove(asset, opts);
},
store() {
return this.getProjectData();
},
load(data) {
return this.loadProjectData(data);
},
/**
* Return the Asset Manager Container
* @returns {HTMLElement}
*/
getContainer() {
const bhv = this.__getBehaviour();
return bhv.container || (am && am.el);
},
/**
* Get assets element container
* @returns {HTMLElement}
* @private
*/
getAssetsEl() {
return 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) {
if (this.getConfig().custom) return;
const toRender = assts || this.getAll().models;
if (!am) {
const obj = this.__viewParams();
obj.fu = this.FileUploader();
const el = am && am.el;
am = new AssetsView({
el,
...obj,
});
am.render();
}
assetsVis.reset(toRender);
return this.getContainer();
},
__viewParams() {
return {
collection: assetsVis, // Collection visible in asset manager
globalCollection: assets,
config: c,
module: this,
};
},
/**
* 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, definition) {
this.getAll().addType(id, definition);
},
/**
* Get type
* @param {string} id Type ID
* @returns {Object} Type definition
* @private
*/
getType(id) {
return this.getAll().getType(id);
},
/**
* Get types
* @returns {Array}
* @private
*/
getTypes() {
return this.getAll().getTypes();
},
//-------
AssetsView() {
return am;
},
FileUploader() {
if (!fu) {
fu = new FileUpload(this.__viewParams());
}
return fu;
},
onLoad() {
this.getAll().reset(c.assets);
const { em, events } = this;
em.on(`run:${assetCmd}`, () => this.__propEv(events.open));
em.on(`stop:${assetCmd}`, () => this.__propEv(events.close));
},
postRender(editorView) {
c.dropzone && fu && fu.initDropzone(editorView);
},
/**
* Set new target
* @param {Object} m Model
* @private
* */
setTarget(m) {
assetsVis.target = m;
},
/**
* Set callback after asset was selected
* @param {Object} f Callback function
* @private
* */
onSelect(f) {
assetsVis.onSelect = f;
},
/**
* Set callback to fire when the asset is clicked
* @param {function} func
* @private
*/
onClick(func) {
c.onClick = func;
},
/**
* Set callback to fire when the asset is double clicked
* @param {function} func
* @private
*/
onDblClick(func) {
c.onDblClick = func;
},
__behaviour(opts = {}) {
return (this._bhv = {
...(this._bhv || {}),
...opts,
});
},
__getBehaviour(opts = {}) {
return this._bhv || {};
},
destroy() {
assets.stopListening();
assetsVis.stopListening();
assets.reset();
assetsVis.reset();
fu && fu.remove();
am && am.remove();
[assets, assetsVis, am, fu].forEach(i => (i = null));
this._bhv = {};
this.all = {};
c = {};
},
};
};

463
src/asset_manager/index.ts

@ -0,0 +1,463 @@
/**
* You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/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 FileUpload from './view/FileUploader';
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 events = {
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>;
export default class AssetManager extends ItemManagerModule<AssetManagerConfig, Assets> {
storageKey = 'assets';
Asset = Asset;
Assets = Assets;
assetsVis: Assets;
am?: AssetsView;
fu?: FileUpload;
_bhv?: any;
/**
* Initialize module
* @param {Object} config Configurations
* @private
*/
constructor(em: EditorModel) {
// @ts-ignore
super(em, 'AssetManager', new Assets([], em), events, 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();
if (!bhv.container && !this.getConfig().custom.open) {
return;
}
this.em.trigger(this.events.custom, this.__customData());
}
__customData() {
const bhv = this.__getBehaviour();
return {
am: this,
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 = {}) {
const cmd = this.em.get('Commands');
cmd.run(assetCmd, {
types: ['image'],
select: () => {},
...options,
});
}
/**
* Close the asset manager.
* @example
* assetManager.close();
*/
close() {
const cmd = this.em.get('Commands');
cmd.stop(assetCmd);
}
/**
* Checks if the asset manager is open
* @returns {Boolean}
* @example
* assetManager.isOpen(); // true | false
*/
isOpen() {
const cmd = this.em.get('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]>}
*/
// @ts-ignore
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,
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 FileUpload(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 = {};
}
}

8
src/asset_manager/model/Asset.js → src/asset_manager/model/Asset.ts

@ -8,6 +8,10 @@ import { Model } from '../../common';
* @module docsjs.Asset
*/
export default class Asset extends Model {
static getDefaults() {
return result(this.prototype, 'defaults');
}
defaults() {
return {
type: '',
@ -65,7 +69,3 @@ export default class Asset extends Model {
}
Asset.prototype.idAttribute = 'src';
Asset.getDefaults = function () {
return result(this.prototype, 'defaults');
};

0
src/asset_manager/model/AssetImage.js → src/asset_manager/model/AssetImage.ts

7
src/asset_manager/model/Assets.js → src/asset_manager/model/Assets.ts

@ -1,16 +1,19 @@
import { Collection } from '../../common';
import Asset from './Asset';
import AssetImage from './AssetImage';
import AssetImageView from './../view/AssetImageView';
import TypeableCollection from '../../domain_abstract/model/TypeableCollection';
export default class Assets extends Collection.extend(TypeableCollection) {}
const TypeableCollectionExt = Collection.extend(TypeableCollection);
export default class Assets extends TypeableCollectionExt<Asset> {}
Assets.prototype.types = [
{
id: 'image',
model: AssetImage,
view: AssetImageView,
isType(value) {
isType(value: string) {
if (typeof value == 'string') {
return {
type: 'image',

11
src/asset_manager/view/AssetImageView.js → src/asset_manager/view/AssetImageView.ts

@ -1,8 +1,9 @@
import { isFunction } from 'underscore';
import AssetView from './AssetView';
import AssetImage from '../model/AssetImage';
import html from '../../utils/html';
export default class AssetImageView extends AssetView {
export default class AssetImageView extends AssetView<AssetImage> {
getPreview() {
const { pfx, ppfx, model } = this;
const src = model.get('src');
@ -26,6 +27,7 @@ export default class AssetImageView extends AssetView {
`;
}
// @ts-ignore
init(o) {
const pfx = this.pfx;
this.className += ` ${pfx}asset-image`;
@ -38,6 +40,7 @@ export default class AssetImageView extends AssetView {
onClick() {
const { model, pfx } = this;
const { select } = this.__getBhv();
// @ts-ignore
const { onClick } = this.config;
const coll = this.collection;
coll.trigger('deselectAll');
@ -48,6 +51,7 @@ export default class AssetImageView extends AssetView {
} else if (isFunction(onClick)) {
onClick(model);
} else {
// @ts-ignore
this.updateTarget(coll.target);
}
}
@ -59,7 +63,9 @@ export default class AssetImageView extends AssetView {
onDblClick() {
const { em, model } = this;
const { select } = this.__getBhv();
// @ts-ignore
const { onDblClick } = this.config;
// @ts-ignore
const { target, onSelect } = this.collection;
if (isFunction(select)) {
@ -77,13 +83,14 @@ export default class AssetImageView extends AssetView {
* Remove asset from collection
* @private
* */
onRemove(e) {
onRemove(e: Event) {
e.stopImmediatePropagation();
this.model.collection.remove(this.model);
}
}
AssetImageView.prototype.events = {
// @ts-ignore
'click [data-toggle=asset-remove]': 'onRemove',
click: 'onClick',
dblclick: 'onDblClick',

40
src/asset_manager/view/AssetView.js → src/asset_manager/view/AssetView.ts

@ -1,20 +1,40 @@
import { View } from '../../common';
import Asset from '../model/Asset';
import Assets from '../model/Assets';
import { AssetManagerConfig } from '../config/config';
import { clone } from 'underscore';
import EditorModel from '../../editor/model/Editor';
export default class AssetView extends View {
initialize(o = {}) {
this.options = o;
this.collection = o.collection;
const config = o.config || {};
export type AssetViewProps = Backbone.ViewOptions<Asset> & {
collection: Assets;
config: AssetManagerConfig;
};
export default class AssetView<TModel extends Asset = Asset> extends View<TModel> {
pfx: string;
ppfx: string;
options: AssetViewProps;
config: AssetManagerConfig;
em: EditorModel;
init?: (opt: AssetViewProps) => void;
constructor(opt: AssetViewProps) {
super(opt as any);
this.options = opt;
this.collection = opt.collection;
const config = opt.config || {};
this.config = config;
this.pfx = config.stylePrefix || '';
// @ts-ignore
this.ppfx = config.pStylePrefix || '';
// @ts-ignore
this.em = config.em;
this.className = this.pfx + 'asset';
this.listenTo(this.model, 'destroy remove', this.remove);
// @ts-ignore
this.model.view = this;
const init = this.init && this.init.bind(this);
init && init(o);
init && init(opt);
}
__getBhv() {
@ -23,8 +43,8 @@ export default class AssetView extends View {
return (am && am.__getBehaviour()) || {};
}
template() {
const pfx = this.pfx;
template(view: AssetView, asset: Asset) {
const { pfx } = this;
return `
<div class="${pfx}preview-cont">
${this.getPreview()}
@ -43,7 +63,7 @@ export default class AssetView extends View {
* @param {Model} target
* @private
* */
updateTarget(target) {
updateTarget(target: any) {
if (target && target.set) {
target.set('attributes', clone(target.get('attributes')));
target.set('src', this.model.get('src'));
@ -61,7 +81,7 @@ export default class AssetView extends View {
render() {
const el = this.el;
el.innerHTML = this.template(this, this.model);
el.className = this.className;
el.className = this.className!;
return this;
}
}

49
src/asset_manager/view/AssetsView.js → src/asset_manager/view/AssetsView.ts

@ -1,15 +1,25 @@
import { View } from '../../common';
import EditorModel from '../../editor/model/Editor';
import { AssetManagerConfig } from '../config/config';
import Asset from '../model/Asset';
export default class AssetsView extends View {
template({ pfx, ppfx, em }) {
options: any;
config: AssetManagerConfig;
pfx: string;
ppfx: string;
em: EditorModel;
inputUrl?: HTMLInputElement | null;
template({ pfx, ppfx, em }: AssetsView) {
let form = '';
if (this.config.showUrlInput) {
form = `
<form class="${pfx}add-asset">
<div class="${ppfx}field ${pfx}add-field">
<input placeholder="${em && em.t('assetManager.inputPlh')}"/>
<input placeholder="${em?.t('assetManager.inputPlh')}"/>
</div>
<button class="${ppfx}btn-prim">${em && em.t('assetManager.addButton')}</button>
<button class="${ppfx}btn-prim">${em?.t('assetManager.addButton')}</button>
<div style="clear:both"></div>
</form>
`;
@ -26,11 +36,14 @@ export default class AssetsView extends View {
`;
}
initialize(o) {
constructor(o: any = {}) {
super(o);
this.options = o;
this.config = o.config;
this.pfx = this.config.stylePrefix || '';
// @ts-ignore
this.ppfx = this.config.pStylePrefix || '';
// @ts-ignore
this.em = this.config.em;
const coll = this.collection;
this.listenTo(coll, 'reset', this.renderAssets);
@ -45,8 +58,8 @@ export default class AssetsView extends View {
* @return {this}
* @private
*/
handleSubmit(e) {
e.preventDefault();
handleSubmit(ev: Event) {
ev.preventDefault();
const input = this.getAddInput();
const url = input && input.value.trim();
const handleAdd = this.config.handleAdd;
@ -56,7 +69,11 @@ export default class AssetsView extends View {
}
input.value = '';
this.getAssetsEl().scrollTop = 0;
const assetsEl = this.getAssetsEl();
if (assetsEl) {
assetsEl.scrollTop = 0;
}
if (handleAdd) {
handleAdd.bind(this)(url);
@ -81,7 +98,9 @@ export default class AssetsView extends View {
* @private
*/
getAddInput() {
if (!this.inputUrl || !this.inputUrl.value) this.inputUrl = this.el.querySelector(`.${this.pfx}add-asset input`);
if (!this.inputUrl || !this.inputUrl.value) {
this.inputUrl = this.el.querySelector(`.${this.pfx}add-asset input`);
}
return this.inputUrl;
}
@ -90,7 +109,7 @@ export default class AssetsView extends View {
* @param {Asset} model Removed asset
* @private
*/
removedAsset(model) {
removedAsset(model: Asset) {
if (!this.collection.length) {
this.toggleNoAssets();
}
@ -100,9 +119,9 @@ export default class AssetsView extends View {
* Add asset to collection
* @private
* */
addToAsset(model) {
addToAsset(model: Asset) {
if (this.collection.length == 1) {
this.toggleNoAssets(1);
this.toggleNoAssets(true);
}
this.addAsset(model);
}
@ -114,10 +133,11 @@ export default class AssetsView extends View {
* @return Object Object created
* @private
* */
addAsset(model, fragmentEl = null) {
addAsset(model: Asset, fragmentEl: DocumentFragment | null = null) {
const fragment = fragmentEl;
const collection = this.collection;
const config = this.config;
// @ts-ignore
const rendered = new model.typeView({
model,
collection,
@ -141,7 +161,7 @@ export default class AssetsView extends View {
* @param {Boolean} hide
* @private
*/
toggleNoAssets(hide) {
toggleNoAssets(hide: boolean = false) {
const assetsEl = this.$el.find(`.${this.pfx}assets`);
if (hide) {
@ -165,7 +185,7 @@ export default class AssetsView extends View {
const fragment = document.createDocumentFragment();
const assets = this.$el.find(`.${this.pfx}assets`);
assets.empty();
this.toggleNoAssets(this.collection.length);
this.toggleNoAssets(!!this.collection.length);
this.collection.each(model => this.addAsset(model, fragment));
assets.append(fragment);
}
@ -181,5 +201,6 @@ export default class AssetsView extends View {
}
AssetsView.prototype.events = {
// @ts-ignore
submit: 'handleSubmit',
};

290
src/asset_manager/view/FileUploader.js → src/asset_manager/view/FileUploader.ts

@ -1,13 +1,36 @@
import { View } from '../../common';
import EditorModel from '../../editor/model/Editor';
import fetch from '../../utils/fetch';
import html from '../../utils/html';
import { AssetManagerConfig } from '../config/config';
type FileUploaderTemplateProps = {
pfx: string;
title: string;
uploadId: string;
disabled: boolean;
multiUpload: boolean;
};
export default class FileUploaderView extends View {
template({ pfx, title, uploadId, disabled, multiUpload }) {
options: any;
config: AssetManagerConfig;
pfx: string;
ppfx: string;
em: EditorModel;
module: any;
target: any;
uploadId: string;
disabled: boolean;
multiUpload: boolean;
uploadForm?: HTMLFormElement | null;
template({ pfx, title, uploadId, disabled, multiUpload }: FileUploaderTemplateProps) {
return html`
<form>
<div id="${pfx}title">${title}</div>
<input
data-input
type="file"
id="${uploadId}"
name="file"
@ -20,11 +43,19 @@ export default class FileUploaderView extends View {
`;
}
initialize(opts = {}) {
events() {
return {
'change [data-input]': 'uploadFile',
};
}
constructor(opts: any = {}) {
super(opts);
this.options = opts;
const c = opts.config || {};
this.module = opts.module;
this.config = c;
// @ts-ignore
this.em = this.config.em;
this.pfx = c.stylePrefix || '';
this.ppfx = c.pStylePrefix || '';
@ -32,15 +63,12 @@ export default class FileUploaderView extends View {
this.uploadId = this.pfx + 'uploadFile';
this.disabled = c.disableUpload !== undefined ? c.disableUpload : !c.upload && !c.embedAsBase64;
this.multiUpload = c.multiUpload !== undefined ? c.multiUpload : true;
this.events = {
[`change #${this.uploadId}`]: 'uploadFile',
};
let uploadFile = c.uploadFile;
const uploadFile = c.uploadFile;
if (uploadFile) {
this.uploadFile = uploadFile.bind(this);
} else if (!c.upload && c.embedAsBase64) {
this.uploadFile = this.constructor.embedAsBase64;
this.uploadFile = FileUploaderView.embedAsBase64;
}
this.delegateEvents();
@ -60,7 +88,7 @@ export default class FileUploaderView extends View {
* @param {Object|string} res End result
* @private
*/
onUploadEnd(res) {
onUploadEnd(res: any) {
const { $el, module } = this;
module && module.__propEv('asset:upload:end', res);
const input = $el.find('input');
@ -72,7 +100,7 @@ export default class FileUploaderView extends View {
* @param {Object} err Error
* @private
*/
onUploadError(err) {
onUploadError(err: Error) {
const { module } = this;
console.error(err);
this.onUploadEnd(err);
@ -84,7 +112,7 @@ export default class FileUploaderView extends View {
* @param {string} text Response text
* @private
*/
onUploadResponse(text, clb) {
onUploadResponse(text: string, clb?: (json: any) => void) {
const { module, config, target } = this;
let json;
try {
@ -100,7 +128,7 @@ export default class FileUploaderView extends View {
}
this.onUploadEnd(text);
clb && clb(json);
clb?.(json);
}
/**
@ -109,7 +137,8 @@ export default class FileUploaderView extends View {
* @return {Promise}
* @private
* */
uploadFile(e, clb) {
uploadFile(e: DragEvent, clb?: () => void) {
// @ts-ignore
const files = e.dataTransfer ? e.dataTransfer.files : e.target.files;
const { config } = this;
const { beforeUpload } = config;
@ -129,12 +158,12 @@ export default class FileUploaderView extends View {
body.append(`${config.uploadName}[]`, files[i]);
}
} else if (files.length) {
body.append(config.uploadName, files[0]);
body.append(config.uploadName!, files[0]);
}
var target = this.target;
const url = config.upload;
const headers = config.headers;
const headers = config.headers!;
const reqHead = 'X-Requested-With';
if (typeof headers[reqHead] == 'undefined') {
@ -151,10 +180,12 @@ export default class FileUploaderView extends View {
};
const fetchResult = customFetch
? customFetch(url, fetchOpts)
: fetch(url, fetchOpts).then(res =>
((res.status / 200) | 0) == 1 ? res.text() : res.text().then(text => Promise.reject(text))
: fetch(url, fetchOpts).then((res: any) =>
((res.status / 200) | 0) == 1 ? res.text() : res.text().then((text: string) => Promise.reject(text))
);
return fetchResult.then(text => this.onUploadResponse(text, clb)).catch(err => this.onUploadError(err));
return fetchResult
.then((text: string) => this.onUploadResponse(text, clb))
.catch((err: Error) => this.onUploadError(err));
}
}
@ -164,29 +195,31 @@ export default class FileUploaderView extends View {
* */
initDrop() {
var that = this;
if (!this.uploadForm) {
this.uploadForm = this.$el.find('form').get(0);
if ('draggable' in this.uploadForm) {
var uploadFile = this.uploadFile;
this.uploadForm = this.$el.find('form').get(0)!;
const formEl = this.uploadForm;
if ('draggable' in formEl) {
this.uploadForm.ondragover = function () {
this.className = that.pfx + 'hover';
formEl.className = that.pfx + 'hover';
return false;
};
this.uploadForm.ondragleave = function () {
this.className = '';
formEl.className = '';
return false;
};
this.uploadForm.ondrop = function (e) {
this.className = '';
e.preventDefault();
that.uploadFile(e);
this.uploadForm.ondrop = function (ev) {
formEl.className = '';
ev.preventDefault();
that.uploadFile(ev);
return;
};
}
}
}
initDropzone(ev) {
initDropzone(ev: any) {
let addedCls = 0;
const c = this.config;
const em = ev.model;
@ -212,7 +245,7 @@ export default class FileUploaderView extends View {
cleanEditorElCls();
return false;
};
const onDrop = e => {
const onDrop = (e: DragEvent) => {
cleanEditorElCls();
e.preventDefault();
e.stopPropagation();
@ -259,111 +292,116 @@ export default class FileUploaderView extends View {
$el.attr('class', pfx + 'file-uploader');
return this;
}
}
FileUploaderView.embedAsBase64 = function (e, clb) {
// List files dropped
const files = e.dataTransfer ? e.dataTransfer.files : e.target.files;
const response = { data: [] };
static embedAsBase64(e: DragEvent, clb?: () => void) {
// List files dropped
// @ts-ignore
const files = e.dataTransfer ? e.dataTransfer.files : e.target.files;
const response: Record<string, any> = { data: [] };
// Unlikely, widely supported now
if (!FileReader) {
this.onUploadError(new Error('Unsupported platform, FileReader is not defined'));
return;
}
// Unlikely, widely supported now
if (!FileReader) {
// @ts-ignore
this.onUploadError(new Error('Unsupported platform, FileReader is not defined'));
return;
}
const promises = [];
const mimeTypeMatcher = /^(.+)\/(.+)$/;
for (const file of files) {
// For each file a reader (to read the base64 URL)
// and a promise (to track and merge results and errors)
const promise = new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', event => {
let type;
const name = file.name;
// Try to find the MIME type of the file.
const match = mimeTypeMatcher.exec(file.type);
if (match) {
type = match[1]; // The first part in the MIME, "image" in image/png
} else {
type = file.type;
const promises = [];
const mimeTypeMatcher = /^(.+)\/(.+)$/;
for (const file of files) {
// For each file a reader (to read the base64 URL)
// and a promise (to track and merge results and errors)
const promise = new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', event => {
let type;
const name = file.name;
// Try to find the MIME type of the file.
const match = mimeTypeMatcher.exec(file.type);
if (match) {
type = match[1]; // The first part in the MIME, "image" in image/png
} else {
type = file.type;
}
/*
// Show local video files, http://jsfiddle.net/dsbonev/cCCZ2/embedded/result,js,html,css/
var URL = window.URL || window.webkitURL
var file = this.files[0]
var type = file.type
var videoNode = document.createElement('video');
var canPlay = videoNode.canPlayType(type) // can use also for 'audio' types
if (canPlay === '') canPlay = 'no'
var message = 'Can play type "' + type + '": ' + canPlay
var isError = canPlay === 'no'
displayMessage(message, isError)
if (isError) {
return
}
/*
// Show local video files, http://jsfiddle.net/dsbonev/cCCZ2/embedded/result,js,html,css/
var URL = window.URL || window.webkitURL
var file = this.files[0]
var type = file.type
var videoNode = document.createElement('video');
var canPlay = videoNode.canPlayType(type) // can use also for 'audio' types
if (canPlay === '') canPlay = 'no'
var message = 'Can play type "' + type + '": ' + canPlay
var isError = canPlay === 'no'
displayMessage(message, isError)
if (isError) {
return
}
var fileURL = URL.createObjectURL(file)
videoNode.src = fileURL
*/
// If it's an image, try to find its size
if (type === 'image') {
const data = {
src: reader.result,
name,
type,
height: 0,
width: 0,
};
const image = new Image();
image.addEventListener('error', error => {
reject(error);
});
image.addEventListener('load', () => {
data.height = image.height;
data.width = image.width;
resolve(data);
});
// @ts-ignore
image.src = data.src;
} else if (type) {
// Not an image, but has a type
resolve({
src: reader.result,
name,
type,
});
} else {
// No type found, resolve with the URL only
resolve(reader.result);
}
});
reader.addEventListener('error', error => {
reject(error);
});
reader.addEventListener('abort', error => {
reject('Aborted');
});
var fileURL = URL.createObjectURL(file)
videoNode.src = fileURL
*/
// If it's an image, try to find its size
if (type === 'image') {
const data = {
src: reader.result,
name,
type,
height: 0,
width: 0,
};
const image = new Image();
image.addEventListener('error', error => {
reject(error);
});
image.addEventListener('load', () => {
data.height = image.height;
data.width = image.width;
resolve(data);
});
image.src = data.src;
} else if (type) {
// Not an image, but has a type
resolve({
src: reader.result,
name,
type,
});
} else {
// No type found, resolve with the URL only
resolve(reader.result);
}
});
reader.addEventListener('error', error => {
reject(error);
});
reader.addEventListener('abort', error => {
reject('Aborted');
reader.readAsDataURL(file);
});
reader.readAsDataURL(file);
});
promises.push(promise);
}
promises.push(promise);
Promise.all(promises).then(
data => {
response.data = data;
// @ts-ignore
this.onUploadResponse(response, clb);
},
error => {
// @ts-ignore
this.onUploadError(error);
}
);
}
Promise.all(promises).then(
data => {
response.data = data;
this.onUploadResponse(response, clb);
},
error => {
this.onUploadError(error);
}
);
};
}

3
test/specs/asset_manager/index.js

@ -1,4 +1,5 @@
import AssetManager from 'asset_manager';
import Editor from 'editor';
describe('Asset Manager', () => {
describe('Main', () => {
@ -13,7 +14,7 @@ describe('Asset Manager', () => {
width: 101,
height: 102,
};
obj = new AssetManager();
obj = new AssetManager(new Editor());
obj.init();
document.body.querySelector('#asset-c').appendChild(obj.render());
});

Loading…
Cancel
Save